From c193f7b041386fda450990a03e6597825425766e Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 18 Dec 2025 16:10:35 +0000 Subject: [PATCH 01/18] feat: add cli template scaffold --- .clang-format | 6 + .clang-tidy | 13 + .devcontainer/devcontainer.json | 36 +++ .devcontainer/postCreate.sh | 65 +++++ .editorconfig | 23 ++ .github/ISSUE_TEMPLATE/bug_report.md | 21 ++ .github/ISSUE_TEMPLATE/feature_request.md | 14 ++ .github/PULL_REQUEST_TEMPLATE.md | 10 + .github/workflows/ci.yml | 76 ++++++ .gitignore | 277 ++++++++++++++++++++++ .pre-commit-config.yaml | 49 ++++ .vscode/settings.json | 43 ++++ CHANGELOG.md | 9 + README.md | 47 ++++ assets/README.md | 4 + completions/README.md | 4 + docs/README.md | 4 + examples/README.md | 4 + man/README.md | 4 + native/cpp/CMakeLists.txt | 21 ++ native/cpp/src/echo.cpp | 22 ++ native/cpp/src/hello.cpp | 10 + native/cpp/src/info.cpp | 12 + native/cpp/src/main.cpp | 44 ++++ native/cpp/src/sum.cpp | 39 +++ native/rust/Cargo.lock | 7 + native/rust/Cargo.toml | 7 + native/rust/rustfmt.toml | 3 + native/rust/src/echo.rs | 10 + native/rust/src/hello.rs | 6 + native/rust/src/info.rs | 6 + native/rust/src/main.rs | 46 ++++ native/rust/src/sum.rs | 25 ++ pyproject.toml | 44 ++++ scripts/README.md | 6 + src/README.md | 6 + src/cli_template/__init__.py | 3 + src/cli_template/__main__.py | 6 + src/cli_template/_native.py | 64 +++++ src/cli_template/cli.py | 102 ++++++++ tests/README.md | 7 + 41 files changed, 1205 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/postCreate.sh create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/settings.json create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 assets/README.md create mode 100644 completions/README.md create mode 100644 docs/README.md create mode 100644 examples/README.md create mode 100644 man/README.md create mode 100644 native/cpp/CMakeLists.txt create mode 100644 native/cpp/src/echo.cpp create mode 100644 native/cpp/src/hello.cpp create mode 100644 native/cpp/src/info.cpp create mode 100644 native/cpp/src/main.cpp create mode 100644 native/cpp/src/sum.cpp create mode 100644 native/rust/Cargo.lock create mode 100644 native/rust/Cargo.toml create mode 100644 native/rust/rustfmt.toml create mode 100644 native/rust/src/echo.rs create mode 100644 native/rust/src/hello.rs create mode 100644 native/rust/src/info.rs create mode 100644 native/rust/src/main.rs create mode 100644 native/rust/src/sum.rs create mode 100644 pyproject.toml create mode 100644 scripts/README.md create mode 100644 src/README.md create mode 100644 src/cli_template/__init__.py create mode 100644 src/cli_template/__main__.py create mode 100644 src/cli_template/_native.py create mode 100644 src/cli_template/cli.py create mode 100644 tests/README.md diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..f4e7c80 --- /dev/null +++ b/.clang-format @@ -0,0 +1,6 @@ +BasedOnStyle: LLVM +ColumnLimit: 100 +IndentWidth: 2 +ReflowComments: true +SortIncludes: true + diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..02a59b4 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,13 @@ +Checks: > + -*, + bugprone-*, + performance-*, + portability-*, + readability-*, + modernize-*, + cppcoreguidelines-*, + -cppcoreguidelines-avoid-magic-numbers +WarningsAsErrors: "" +HeaderFilterRegex: "native/cpp/.*" +FormatStyle: file + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..74d4acc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,36 @@ +{ + "name": "cli-template", + "image": "mcr.microsoft.com/devcontainers/base:bookworm", + "features": { + "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { + "packages": "build-essential,cmake,ninja-build,clang,clang-format,clang-tidy,cppcheck" + }, + "ghcr.io/devcontainers/features/python:1": {}, + "ghcr.io/devcontainers/features/rust:1": {}, + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/home/vscode/.local/bin:/home/vscode/.cargo/bin" + }, + "postCreateCommand": "bash .devcontainer/postCreate.sh", + "remoteUser": "vscode", + "updateRemoteUserUID": true, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "charliermarsh.ruff", + "rust-lang.rust-analyzer", + "vadimcn.vscode-lldb", + "tamasfe.even-better-toml", + "redhat.vscode-yaml", + "github.vscode-github-actions", + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "xaver.clang-format", + "GitHub.vscode-pull-request-github" + ] + } + } +} diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh new file mode 100755 index 0000000..b97f5e9 --- /dev/null +++ b/.devcontainer/postCreate.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" + +if command -v python >/dev/null 2>&1; then + python_cmd="python" +elif command -v python3 >/dev/null 2>&1; then + python_cmd="python3" +else + echo "ERROR: expected 'python' or 'python3' to be installed in the devcontainer." >&2 + exit 1 +fi + +required_bins=(cargo node gh clang-format clang-tidy cppcheck cmake) +for bin in "${required_bins[@]}"; do + if ! command -v "$bin" >/dev/null 2>&1; then + echo "ERROR: expected '$bin' to be installed in the devcontainer." >&2 + exit 1 + fi +done + +echo "Devcontainer tool versions:" +"$python_cmd" --version +cargo --version +rustc --version +node --version +gh --version +clang-format --version +clang-tidy --version +cppcheck --version +cmake --version | head -n 1 + +# Remove PEP 668 restriction in dev container (container is isolated and disposable) +EXTERNALLY_MANAGED=$("$python_cmd" -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')/EXTERNALLY-MANAGED +if [ -f "$EXTERNALLY_MANAGED" ]; then + sudo rm -f "$EXTERNALLY_MANAGED" +fi + +# Install project with dev dependencies (includes ruff, pre-commit, commitizen) +if [ -f "pyproject.toml" ]; then + "$python_cmd" -m pip install --user --upgrade pip + "$python_cmd" -m pip install --user -e ".[dev]" +fi + +# Display installed tool versions +if command -v ruff >/dev/null 2>&1; then + ruff --version +fi + +if command -v pre-commit >/dev/null 2>&1; then + pre-commit --version +fi + +if command -v cz >/dev/null 2>&1; then + cz version +fi + +if command -v rustup >/dev/null 2>&1; then + rustup component add rustfmt clippy >/dev/null +fi + +if [ -f "native/cpp/CMakeLists.txt" ]; then + cmake -S native/cpp -B build/native-cpp -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON >/dev/null +fi diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1ce7049 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.toml] +indent_style = space +indent_size = 2 + +[*.rs] +indent_style = space +indent_size = 4 + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..3d60331 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Report a reproducible problem +labels: bug +--- + +## What happened? + +## What did you expect to happen? + +## Steps to reproduce + +1. +2. +3. + +## Environment + +- OS: +- Version: + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..1da7962 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea or enhancement +labels: enhancement +--- + +## Problem + +What problem are you trying to solve? + +## Proposal + +What would you like to see added/changed? + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3295e83 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +## Summary + +Describe what this PR changes and why. + +## Checklist + +- [ ] Tests added/updated (if applicable) +- [ ] Docs updated (if applicable) +- [ ] `pre-commit run --all-files` passes +- [ ] `cargo test` passes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b8cd469 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install C++ tools + run: | + sudo apt-get update + sudo apt-get install -y clang-format cppcheck + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Install pre-commit + run: python -m pip install --upgrade pip pre-commit + + - name: Run pre-commit + run: pre-commit run --all-files --show-diff-on-failure + + native-rust: + runs-on: ubuntu-latest + needs: pre-commit + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Clippy + run: cargo clippy --manifest-path native/rust/Cargo.toml --all-targets -- -D warnings + + - name: Test + run: cargo test --manifest-path native/rust/Cargo.toml --all-targets + + native-cpp: + runs-on: ubuntu-latest + needs: pre-commit + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake ninja-build + + - name: Configure + run: cmake -S native/cpp -B build/native-cpp -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: Build + run: cmake --build build/native-cpp + + - name: Smoke test + run: ./build/native-cpp/cli-template-native-cpp hello alice diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..419bb7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,277 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +# Rust +# Generated by Cargo +# will have compiled files and executables +debug + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# C/C++ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Linker files +*.ilk + +# Compiled Dynamic libraries +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# debug information files +*.dwo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..db03098 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,49 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.9 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format + + - repo: https://github.com/commitizen-tools/commitizen + rev: v4.10.0 + hooks: + - id: commitizen + stages: [commit-msg] + + - repo: local + hooks: + - id: cargo-fmt + name: cargo fmt + entry: cargo fmt --manifest-path native/rust/Cargo.toml + language: system + types: [rust] + pass_filenames: false + + - id: cargo-clippy + name: cargo clippy + entry: cargo clippy --manifest-path native/rust/Cargo.toml --all-targets -- -D warnings + language: system + types: [rust] + pass_filenames: false + + - id: clang-format + name: clang-format + entry: clang-format -i -style=file + language: system + types_or: [c, "c++"] + + - id: cppcheck + name: cppcheck + entry: cppcheck --enable=warning,style,performance,portability --std=c++20 --inline-suppr --error-exitcode=1 --quiet --suppress=missingIncludeSystem + language: system + types_or: [c, "c++"] diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0d3f929 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,43 @@ +{ + "editor.formatOnSave": true, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "python.analysis.typeCheckingMode": "basic", + "python.defaultInterpreterPath": "python3", + "ruff.nativeServer": true, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll.ruff": "explicit", + "source.organizeImports.ruff": "explicit" + } + }, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + }, + "rust-analyzer.check.command": "clippy", + "rust-analyzer.linkedProjects": [ + "native/rust/Cargo.toml" + ], + "cmake.sourceDirectory": "${workspaceFolder}/native/cpp", + "cmake.buildDirectory": "${workspaceFolder}/build/native-cpp", + "cmake.generator": "Ninja", + "cmake.configureOnOpen": true, + "cmake.configureSettings": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", + "C_Cpp.default.compileCommands": "${workspaceFolder}/build/native-cpp/compile_commands.json", + "C_Cpp.default.cppStandard": "c++20", + "C_Cpp.codeAnalysis.clangTidy.enabled": true, + "[cpp]": { + "editor.defaultFormatter": "ms-vscode.cpptools" + }, + "[c]": { + "editor.defaultFormatter": "ms-vscode.cpptools" + }, + "[markdown]": { + "editor.defaultFormatter": "vscode.markdown-language-features" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..01af960 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + diff --git a/README.md b/README.md new file mode 100644 index 0000000..72efd3d --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# cli-template + +Production-ready starting point for building a CLI, with: + +- Dev Container + VS Code defaults for Python (`ruff`), Rust (`rust-analyzer`, `clippy`) and C++ (`clang-format`, `cppcheck`) +- `pre-commit` hooks for formatting/linting +- `commitizen` for Conventional Commits + changelog/version bumping + +## Quickstart + +1. Open in VS Code +2. Run **Dev Containers: Reopen in Container** +3. Install git hooks: + + - `pre-commit install` + - `pre-commit install --hook-type commit-msg` + +## Run the CLI + +- `python -m cli_template hello --name alice` +- `python -m cli_template echo hello world` +- `python -m cli_template sum 2 3` +- `python -m cli_template info` + +## Optional native backends + +Build the native backends and switch implementations with `--impl`. + +### Rust backend + +- Build: `cargo build --manifest-path native/rust/Cargo.toml --release` +- Run: `python -m cli_template --impl rust hello --name alice` +- Run: `python -m cli_template --impl rust sum 2 3` + +### C++ backend + +- Build: + - `cmake -S native/cpp -B build/native-cpp -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON` + - `cmake --build build/native-cpp` +- Run: `python -m cli_template --impl cpp hello --name alice` +- Run: `python -m cli_template --impl cpp echo hello world` + +## Commit conventions + +- Create a commit: `cz commit` +- Check your last commit message: `cz check -r HEAD` +- Bump version + update changelog: `cz bump` diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 0000000..8434041 --- /dev/null +++ b/assets/README.md @@ -0,0 +1,4 @@ +# Assets + +Non-code assets bundled with the CLI (templates, default config files, etc.). + diff --git a/completions/README.md b/completions/README.md new file mode 100644 index 0000000..e104f91 --- /dev/null +++ b/completions/README.md @@ -0,0 +1,4 @@ +# Shell completions + +Store generated shell completion scripts here (bash/zsh/fish). + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..79ae412 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,4 @@ +# Docs + +Put longer-form documentation here (guides, design notes, decision records). + diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..2127c86 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,4 @@ +# Examples + +Put runnable examples here (sample configs, sample input/output). + diff --git a/man/README.md b/man/README.md new file mode 100644 index 0000000..8f24a9d --- /dev/null +++ b/man/README.md @@ -0,0 +1,4 @@ +# Manpages + +Store `man` page sources/output here (e.g. `man1/.1`). + diff --git a/native/cpp/CMakeLists.txt b/native/cpp/CMakeLists.txt new file mode 100644 index 0000000..7523d7e --- /dev/null +++ b/native/cpp/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.20) + +project(cli_template_native_cpp VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(cli_template_native_cpp src/main.cpp) +set_target_properties(cli_template_native_cpp PROPERTIES OUTPUT_NAME "cli-template-native-cpp") + +target_sources( + cli_template_native_cpp + PRIVATE + src/echo.cpp + src/hello.cpp + src/info.cpp + src/sum.cpp +) + +target_compile_definitions(cli_template_native_cpp PRIVATE CLI_TEMPLATE_VERSION="${PROJECT_VERSION}") + diff --git a/native/cpp/src/echo.cpp b/native/cpp/src/echo.cpp new file mode 100644 index 0000000..94ea0c3 --- /dev/null +++ b/native/cpp/src/echo.cpp @@ -0,0 +1,22 @@ +#include +#include + +int cmd_echo(int argc, char** argv) { + // argv[0] == "echo" + if (argc < 2) { + std::cerr << "usage: cli-template-native-cpp echo " << std::endl; + return 2; + } + + std::string out; + for (int i = 1; i < argc; i++) { + if (!out.empty()) { + out += ' '; + } + out += argv[i]; + } + + std::cout << out << std::endl; + return 0; +} + diff --git a/native/cpp/src/hello.cpp b/native/cpp/src/hello.cpp new file mode 100644 index 0000000..ec63004 --- /dev/null +++ b/native/cpp/src/hello.cpp @@ -0,0 +1,10 @@ +#include +#include + +int cmd_hello(int argc, char** argv) { + // argv[0] == "hello" + std::string name = argc > 1 ? argv[1] : "world"; + std::cout << "hello, " << name << std::endl; + return 0; +} + diff --git a/native/cpp/src/info.cpp b/native/cpp/src/info.cpp new file mode 100644 index 0000000..8dcdd5f --- /dev/null +++ b/native/cpp/src/info.cpp @@ -0,0 +1,12 @@ +#include + +#ifndef CLI_TEMPLATE_VERSION +#define CLI_TEMPLATE_VERSION "0.0.0" +#endif + +int cmd_info(int /*argc*/, char** /*argv*/) { + std::cout << "backend=cpp" << std::endl; + std::cout << "version=" << CLI_TEMPLATE_VERSION << std::endl; + return 0; +} + diff --git a/native/cpp/src/main.cpp b/native/cpp/src/main.cpp new file mode 100644 index 0000000..cf18bda --- /dev/null +++ b/native/cpp/src/main.cpp @@ -0,0 +1,44 @@ +#include +#include + +int cmd_echo(int argc, char** argv); +int cmd_hello(int argc, char** argv); +int cmd_info(int argc, char** argv); +int cmd_sum(int argc, char** argv); + +static void print_help() { + std::cerr << "cli-template-native-cpp (native C++ backend)\n\n"; + std::cerr << "Usage:\n"; + std::cerr << " cli-template-native-cpp [args]\n\n"; + std::cerr << "Commands:\n"; + std::cerr << " hello [name] Print a friendly greeting\n"; + std::cerr << " echo Echo the message\n"; + std::cerr << " sum Print the sum of two integers\n"; + std::cerr << " info Print backend info\n"; +} + +int main(int argc, char** argv) { + std::string command = argc > 1 ? argv[1] : "--help"; + + if (command == "-h" || command == "--help" || command == "help") { + print_help(); + return 0; + } + + if (command == "hello") { + return cmd_hello(argc - 1, argv + 1); + } + if (command == "echo") { + return cmd_echo(argc - 1, argv + 1); + } + if (command == "sum") { + return cmd_sum(argc - 1, argv + 1); + } + if (command == "info") { + return cmd_info(argc - 1, argv + 1); + } + + std::cerr << "unknown command: " << command << std::endl << std::endl; + print_help(); + return 2; +} diff --git a/native/cpp/src/sum.cpp b/native/cpp/src/sum.cpp new file mode 100644 index 0000000..e53bab7 --- /dev/null +++ b/native/cpp/src/sum.cpp @@ -0,0 +1,39 @@ +#include +#include + +static bool parse_i64(const std::string& value, long long* out) { + try { + size_t pos = 0; + long long parsed = std::stoll(value, &pos, 10); + if (pos != value.size()) { + return false; + } + *out = parsed; + return true; + } catch (...) { + return false; + } +} + +int cmd_sum(int argc, char** argv) { + // argv[0] == "sum" + if (argc != 3) { + std::cerr << "usage: cli-template-native-cpp sum " << std::endl; + return 2; + } + + long long a = 0; + long long b = 0; + if (!parse_i64(argv[1], &a)) { + std::cerr << "invalid integer: " << argv[1] << std::endl; + return 2; + } + if (!parse_i64(argv[2], &b)) { + std::cerr << "invalid integer: " << argv[2] << std::endl; + return 2; + } + + std::cout << (a + b) << std::endl; + return 0; +} + diff --git a/native/rust/Cargo.lock b/native/rust/Cargo.lock new file mode 100644 index 0000000..08e6b6b --- /dev/null +++ b/native/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cli-template-native-rs" +version = "0.1.0" diff --git a/native/rust/Cargo.toml b/native/rust/Cargo.toml new file mode 100644 index 0000000..d51ca89 --- /dev/null +++ b/native/rust/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "cli-template-native-rs" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] diff --git a/native/rust/rustfmt.toml b/native/rust/rustfmt.toml new file mode 100644 index 0000000..f811bd2 --- /dev/null +++ b/native/rust/rustfmt.toml @@ -0,0 +1,3 @@ +edition = "2021" +max_width = 100 + diff --git a/native/rust/src/echo.rs b/native/rust/src/echo.rs new file mode 100644 index 0000000..305a64c --- /dev/null +++ b/native/rust/src/echo.rs @@ -0,0 +1,10 @@ +pub fn run(args: &[String]) -> i32 { + if args.is_empty() { + eprintln!("usage: cli-template-native-rs echo "); + return 2; + } + + println!("{}", args.join(" ")); + 0 +} + diff --git a/native/rust/src/hello.rs b/native/rust/src/hello.rs new file mode 100644 index 0000000..0333159 --- /dev/null +++ b/native/rust/src/hello.rs @@ -0,0 +1,6 @@ +pub fn run(args: &[String]) -> i32 { + let name = args.first().map(String::as_str).unwrap_or("world"); + println!("hello, {name}"); + 0 +} + diff --git a/native/rust/src/info.rs b/native/rust/src/info.rs new file mode 100644 index 0000000..38f9f08 --- /dev/null +++ b/native/rust/src/info.rs @@ -0,0 +1,6 @@ +pub fn run() -> i32 { + println!("backend=rust"); + println!("version={}", env!("CARGO_PKG_VERSION")); + 0 +} + diff --git a/native/rust/src/main.rs b/native/rust/src/main.rs new file mode 100644 index 0000000..d9b045d --- /dev/null +++ b/native/rust/src/main.rs @@ -0,0 +1,46 @@ +mod echo; +mod hello; +mod info; +mod sum; + +fn print_help() { + eprintln!("cli-template-native-rs (native Rust backend)"); + eprintln!(); + eprintln!("Usage:"); + eprintln!(" cli-template-native-rs [args]"); + eprintln!(); + eprintln!("Commands:"); + eprintln!(" hello [name] Print a friendly greeting"); + eprintln!(" echo Echo the message"); + eprintln!(" sum Print the sum of two integers"); + eprintln!(" info Print backend info"); +} + +fn main() { + let mut args = std::env::args(); + let _bin = args.next(); + let command = args + .next() + .unwrap_or_else(|| "--help".to_string()) + .to_string(); + let rest: Vec = args.collect(); + + let exit_code = match command.as_str() { + "-h" | "--help" | "help" => { + print_help(); + 0 + } + "hello" => hello::run(&rest), + "echo" => echo::run(&rest), + "sum" => sum::run(&rest), + "info" => info::run(), + _ => { + eprintln!("unknown command: {command}"); + eprintln!(); + print_help(); + 2 + } + }; + + std::process::exit(exit_code); +} diff --git a/native/rust/src/sum.rs b/native/rust/src/sum.rs new file mode 100644 index 0000000..7c4b43f --- /dev/null +++ b/native/rust/src/sum.rs @@ -0,0 +1,25 @@ +pub fn run(args: &[String]) -> i32 { + if args.len() != 2 { + eprintln!("usage: cli-template-native-rs sum "); + return 2; + } + + let a: i64 = match args[0].parse() { + Ok(value) => value, + Err(_) => { + eprintln!("invalid integer: {}", args[0]); + return 2; + } + }; + let b: i64 = match args[1].parse() { + Ok(value) => value, + Err(_) => { + eprintln!("invalid integer: {}", args[1]); + return 2; + } + }; + + println!("{}", a + b); + 0 +} + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5a837cb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["hatchling>=1.26.0"] +build-backend = "hatchling.build" + +[project] +name = "cli-template" +dynamic = ["version"] +description = "Python-first CLI template with optional Rust/C++ backends." +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] + +[project.optional-dependencies] +dev = [ + "commitizen>=4.0.0", + "pre-commit>=3.8.0", + "ruff>=0.7.0" +] + +[project.scripts] +cli-template = "cli_template.cli:main" + +[tool.hatch.version] +path = "src/cli_template/__init__.py" + +[tool.ruff] +line-length = 100 +target-version = "py311" +extend-exclude = ["build", "native/rust/target"] + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "UP"] + +[tool.commitizen] +name = "cz_conventional_commits" +version = "0.1.0" +version_scheme = "semver" +tag_format = "v$version" +update_changelog_on_bump = true +changelog_file = "CHANGELOG.md" +version_files = [ + "src/cli_template/__init__.py:__version__", + "native/rust/Cargo.toml:version" +] diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..8dbf4b2 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,6 @@ +# Scripts + +Helper scripts for development and release automation. + +Recommended: keep scripts idempotent and runnable from repo root. + diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..b08b24a --- /dev/null +++ b/src/README.md @@ -0,0 +1,6 @@ +# Source + +Project source code lives here. + +- This repo is **Python-first**: the user-facing CLI is implemented as a Python package under `src/`. +- Native code is optional and lives under `native/` (Rust/C++). diff --git a/src/cli_template/__init__.py b/src/cli_template/__init__.py new file mode 100644 index 0000000..a05eb9a --- /dev/null +++ b/src/cli_template/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["__version__"] + +__version__ = "0.1.0" diff --git a/src/cli_template/__main__.py b/src/cli_template/__main__.py new file mode 100644 index 0000000..855590e --- /dev/null +++ b/src/cli_template/__main__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from .cli import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/cli_template/_native.py b/src/cli_template/_native.py new file mode 100644 index 0000000..0551e19 --- /dev/null +++ b/src/cli_template/_native.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import os +import subprocess +import sys +from collections.abc import Sequence +from pathlib import Path + + +class NativeBackendError(RuntimeError): + pass + + +def _repo_root() -> Path: + env_root = os.environ.get("CLI_TEMPLATE_REPO_ROOT") + if env_root: + return Path(env_root).expanduser().resolve() + + cwd = Path.cwd() + for candidate in (cwd, *cwd.parents): + if (candidate / "pyproject.toml").is_file(): + return candidate + + raise NativeBackendError( + "Could not locate repo root (pyproject.toml). " + "Run from inside the repo or set CLI_TEMPLATE_REPO_ROOT." + ) + + +def _rust_bin_path(repo_root: Path) -> Path: + return repo_root / "native" / "rust" / "target" / "release" / "cli-template-native-rs" + + +def _cpp_bin_path(repo_root: Path) -> Path: + return repo_root / "build" / "native-cpp" / "cli-template-native-cpp" + + +def run_rust(argv: Sequence[str]) -> int: + repo_root = _repo_root() + binary = _rust_bin_path(repo_root) + if not binary.is_file(): + raise NativeBackendError( + f"Rust backend binary not found at '{binary}'. " + "Build it with: cargo build --manifest-path native/rust/Cargo.toml --release" + ) + result = subprocess.run([str(binary), *argv], check=False) + return result.returncode + + +def run_cpp(argv: Sequence[str]) -> int: + repo_root = _repo_root() + binary = _cpp_bin_path(repo_root) + if sys.platform == "win32": + binary = binary.with_suffix(".exe") + + if not binary.is_file(): + raise NativeBackendError( + f"C++ backend binary not found at '{binary}'. " + "Build it with: cmake -S native/cpp -B build/native-cpp -GNinja " + "-DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON " + "&& cmake --build build/native-cpp" + ) + result = subprocess.run([str(binary), *argv], check=False) + return result.returncode diff --git a/src/cli_template/cli.py b/src/cli_template/cli.py new file mode 100644 index 0000000..80734ca --- /dev/null +++ b/src/cli_template/cli.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import argparse + +from ._native import NativeBackendError, run_cpp, run_rust +from . import __version__ + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="cli-template", description="Python-first CLI template.") + + parser.add_argument( + "--impl", + choices=("python", "rust", "cpp"), + default="python", + help="Implementation backend for commands that support native backends.", + ) + + sub = parser.add_subparsers(dest="command", required=True) + + hello = sub.add_parser("hello", help="Print a friendly greeting.") + hello.add_argument("--name", default="world") + + echo = sub.add_parser("echo", help="Echo the message.") + echo.add_argument("message", nargs="+") + + sum_cmd = sub.add_parser("sum", help="Print the sum of two integers.") + sum_cmd.add_argument("a", type=int) + sum_cmd.add_argument("b", type=int) + + sub.add_parser("info", help="Print backend info.") + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = _build_parser() + args = parser.parse_args(argv) + + if args.command == "hello": + if args.impl == "python": + print(f"hello, {args.name}") + return 0 + if args.impl == "rust": + try: + return run_rust(["hello", args.name]) + except NativeBackendError as exc: + parser.error(str(exc)) + if args.impl == "cpp": + try: + return run_cpp(["hello", args.name]) + except NativeBackendError as exc: + parser.error(str(exc)) + + if args.command == "echo": + if args.impl == "python": + print(" ".join(args.message)) + return 0 + if args.impl == "rust": + try: + return run_rust(["echo", *args.message]) + except NativeBackendError as exc: + parser.error(str(exc)) + if args.impl == "cpp": + try: + return run_cpp(["echo", *args.message]) + except NativeBackendError as exc: + parser.error(str(exc)) + + if args.command == "sum": + if args.impl == "python": + print(args.a + args.b) + return 0 + if args.impl == "rust": + try: + return run_rust(["sum", str(args.a), str(args.b)]) + except NativeBackendError as exc: + parser.error(str(exc)) + if args.impl == "cpp": + try: + return run_cpp(["sum", str(args.a), str(args.b)]) + except NativeBackendError as exc: + parser.error(str(exc)) + + if args.command == "info": + if args.impl == "python": + print("backend=python") + print(f"version={__version__}") + return 0 + if args.impl == "rust": + try: + return run_rust(["info"]) + except NativeBackendError as exc: + parser.error(str(exc)) + if args.impl == "cpp": + try: + return run_cpp(["info"]) + except NativeBackendError as exc: + parser.error(str(exc)) + + parser.error(f"unknown command: {args.command}") + return 2 diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..67459d6 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,7 @@ +# Tests + +Project tests live here. + +- Rust integration tests: `tests/*.rs` +- Python tests: `pytest` tests can also live here. + From 05de1054256240166d7118d1c57ee305167c0853 Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:44:41 +0000 Subject: [PATCH 02/18] chore: add MIT license --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..53f2420 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "[]" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright 2025 The sdk-template Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. From 25bd4da653c9f49407805461d3450d34fe916b4f Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:44:47 +0000 Subject: [PATCH 03/18] docs: add repository guidelines for agents --- AGENTS.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..db9186b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,46 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +- `src/cli_template/`: Python CLI entry (`cli.py`, `__main__.py`), native bridge `_native.py`. +- `native/rust/`: Optional Rust backend; binary at `native/rust/target/release/cli-template-native-rs`. +- `native/cpp/`: Optional C++ backend; build to `build/native-cpp/cli-template-native-cpp`. +- `tests/`: Pytest suite for CLI and native helpers; add cases near the feature. +- `docs/`, `examples/`, `man/`, `completions/`: Update when CLI flags or output change. +- `scripts/`: Automation (e.g., `scripts/setup.sh`); extend instead of duplicating commands. +- `build/`, `htmlcov*`, `coverage*`: Generated; keep untracked. + +## Setup, Build & Local Run + +- Dev env: `bash scripts/setup.sh` (creates `.venv` with uv, installs `.[dev]`). +- Run CLI: `python -m cli_template hello --name alice`; add `--impl rust|cpp` after building natives. +- Rust build: `cargo build --manifest-path native/rust/Cargo.toml --release`. +- C++ build: `cmake -S native/cpp -B build/native-cpp -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON && cmake --build build/native-cpp`. +- Quick checks: `pre-commit run --all-files`. + +## Coding Style & Naming Conventions + +- Python: `ruff format` + `ruff check --fix`, 100-char lines, typed; snake_case modules/functions, PascalCase classes, UPPER_SNAKE_CASE constants. +- Rust: `cargo fmt` and `cargo clippy --manifest-path native/rust/Cargo.toml --all-targets -- -D warnings`; modules snake_case; avoid panics in library code. +- C++: `clang-format` (LLVM, 2-space, 100-col) and `cppcheck`; keep headers self-contained. +- Repo-wide: LF endings, final newline, no trailing whitespace (`.editorconfig`). + +## Testing Guidelines + +- Target 100% coverage across Python, Rust, and C++ (see `tests/README.md`). +- Commands: `pytest --cov=cli_template --cov-report=term-missing`, `cargo test --manifest-path native/rust/Cargo.toml`, `ctest --test-dir build/native-cpp --output-on-failure`. +- Coverage HTML (optional): `pytest --cov-report=html:build/coverage-html`; `cargo llvm-cov --manifest-path native/rust/Cargo.toml --lib --html`; C++ steps in `tests/README.md`. +- Name tests `test_*.py`, `*_test.rs` or module tests, and keep C++ cases under `native/cpp/tests/`; add regressions for every bug fix. + +## Commit & Pull Request Guidelines + +- Use Conventional Commits via `cz commit` (e.g., `feat: add sum flag`); keep scopes small. +- Run `pre-commit run --all-files` and relevant tests before pushing; record results in the PR. +- PRs: concise summary, linked issue/ID, backend(s) touched, sample CLI output for user-facing changes, updated docs/completions/man when flags change. +- Releases: `cz bump` updates version and `CHANGELOG.md`; rebuild native binaries for tags. + +## Security & Configuration Tips + +- Do not commit secrets; `.env` stays local. +- `CLI_TEMPLATE_REPO_ROOT` lets native helpers run from an external workspace. +- Keep build and coverage artifacts untracked; prefer reproducible builds. From 87c085a07792589f6368c73c3b132a780144a7f3 Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:44:52 +0000 Subject: [PATCH 04/18] chore: update devcontainer configuration and setup scripts --- .devcontainer/devcontainer.json | 36 +++++++------ .devcontainer/postCreate.sh | 65 ------------------------ .devcontainer/scripts/clean-workspace.sh | 29 +++++++++++ .devcontainer/scripts/setup.sh | 25 +++++++++ 4 files changed, 74 insertions(+), 81 deletions(-) delete mode 100755 .devcontainer/postCreate.sh create mode 100755 .devcontainer/scripts/clean-workspace.sh create mode 100755 .devcontainer/scripts/setup.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 74d4acc..1c8b2d6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,35 +1,39 @@ { - "name": "cli-template", + "name": "sdk-template", "image": "mcr.microsoft.com/devcontainers/base:bookworm", "features": { - "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { - "packages": "build-essential,cmake,ninja-build,clang,clang-format,clang-tidy,cppcheck" + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12", + "installTools": false }, - "ghcr.io/devcontainers/features/python:1": {}, - "ghcr.io/devcontainers/features/rust:1": {}, - "ghcr.io/devcontainers/features/node:1": {}, - "ghcr.io/devcontainers/features/github-cli:1": {} + "ghcr.io/devcontainers/features/rust:1": { + "version": "latest", + "profile": "default" + }, + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/copilot-cli:1": {}, + "ghcr.io/devcontainers-extra/features/apt-packages:1": { + "packages": "cmake,ninja-build,clang,clang-format,clang-tidy" + } }, - "remoteEnv": { - "PATH": "${containerEnv:PATH}:/home/vscode/.local/bin:/home/vscode/.cargo/bin" + "containerEnv": { + "UV_CACHE_DIR": "/tmp/uv-cache" }, - "postCreateCommand": "bash .devcontainer/postCreate.sh", + "postCreateCommand": "bash .devcontainer/scripts/setup.sh", "remoteUser": "vscode", - "updateRemoteUserUID": true, "customizations": { "vscode": { "extensions": [ "ms-python.python", + "ms-python.vscode-pylance", "charliermarsh.ruff", "rust-lang.rust-analyzer", - "vadimcn.vscode-lldb", - "tamasfe.even-better-toml", - "redhat.vscode-yaml", - "github.vscode-github-actions", "ms-vscode.cpptools", "ms-vscode.cmake-tools", "xaver.clang-format", - "GitHub.vscode-pull-request-github" + "tamasfe.even-better-toml", + "github.vscode-github-actions" ] } } diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh deleted file mode 100755 index b97f5e9..0000000 --- a/.devcontainer/postCreate.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" - -if command -v python >/dev/null 2>&1; then - python_cmd="python" -elif command -v python3 >/dev/null 2>&1; then - python_cmd="python3" -else - echo "ERROR: expected 'python' or 'python3' to be installed in the devcontainer." >&2 - exit 1 -fi - -required_bins=(cargo node gh clang-format clang-tidy cppcheck cmake) -for bin in "${required_bins[@]}"; do - if ! command -v "$bin" >/dev/null 2>&1; then - echo "ERROR: expected '$bin' to be installed in the devcontainer." >&2 - exit 1 - fi -done - -echo "Devcontainer tool versions:" -"$python_cmd" --version -cargo --version -rustc --version -node --version -gh --version -clang-format --version -clang-tidy --version -cppcheck --version -cmake --version | head -n 1 - -# Remove PEP 668 restriction in dev container (container is isolated and disposable) -EXTERNALLY_MANAGED=$("$python_cmd" -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')/EXTERNALLY-MANAGED -if [ -f "$EXTERNALLY_MANAGED" ]; then - sudo rm -f "$EXTERNALLY_MANAGED" -fi - -# Install project with dev dependencies (includes ruff, pre-commit, commitizen) -if [ -f "pyproject.toml" ]; then - "$python_cmd" -m pip install --user --upgrade pip - "$python_cmd" -m pip install --user -e ".[dev]" -fi - -# Display installed tool versions -if command -v ruff >/dev/null 2>&1; then - ruff --version -fi - -if command -v pre-commit >/dev/null 2>&1; then - pre-commit --version -fi - -if command -v cz >/dev/null 2>&1; then - cz version -fi - -if command -v rustup >/dev/null 2>&1; then - rustup component add rustfmt clippy >/dev/null -fi - -if [ -f "native/cpp/CMakeLists.txt" ]; then - cmake -S native/cpp -B build/native-cpp -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON >/dev/null -fi diff --git a/.devcontainer/scripts/clean-workspace.sh b/.devcontainer/scripts/clean-workspace.sh new file mode 100755 index 0000000..f89330c --- /dev/null +++ b/.devcontainer/scripts/clean-workspace.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/../.." + +echo "==> Cleaning workspace caches..." + +# Python caches +find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true +find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true +find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true +find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true +find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true +find . -type f -name ".coverage" -delete 2>/dev/null || true +find . -type f -name "coverage.xml" -delete 2>/dev/null || true +find . -type f -name "coverage.info" -delete 2>/dev/null || true +rm -rf htmlcov/ 2>/dev/null || true +rm -rf .hypothesis/ 2>/dev/null || true + +# Rust caches +rm -rf native/rust/target/ 2>/dev/null || true + +# C++ build artifacts +rm -rf build/ 2>/dev/null || true + +# UV cache +rm -rf .venv/ 2>/dev/null || true + +echo "==> Workspace cleaned!" diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh new file mode 100755 index 0000000..78f16fc --- /dev/null +++ b/.devcontainer/scripts/setup.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "==> Setting up development environment..." + +# Install uv if not already installed +if ! command -v uv &> /dev/null; then + echo "Installing uv..." + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.cargo/bin:$PATH" +fi + +# Install Rust coverage tool +cargo install cargo-llvm-cov --quiet +rustup component add llvm-tools-preview + +# Install Python dependencies +uv sync --dev + +# Configure C++ build +cmake -S native/cpp -B build/native-cpp -GNinja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + +echo "==> Setup complete!" From 790bcf9e2b443bbf61f420ed51033056d2a23170 Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:44:57 +0000 Subject: [PATCH 05/18] chore: add VSCode tasks and update editor settings --- .vscode/settings.json | 13 ++++++-- .vscode/tasks.json | 70 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 .vscode/tasks.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 0d3f929..2c3baf2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,10 @@ "files.trimTrailingWhitespace": true, "python.analysis.typeCheckingMode": "basic", "python.defaultInterpreterPath": "python3", + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": [ + "tests" + ], "ruff.nativeServer": true, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", @@ -25,12 +29,17 @@ "cmake.configureOnOpen": true, "cmake.configureSettings": { "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "BUILD_TESTS": "ON" }, + "cmake.ctest.testExplorerIntegrationEnabled": true, "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", "C_Cpp.default.compileCommands": "${workspaceFolder}/build/native-cpp/compile_commands.json", "C_Cpp.default.cppStandard": "c++20", "C_Cpp.codeAnalysis.clangTidy.enabled": true, + "C_Cpp.codeAnalysis.clangTidy.path": "", + "C_Cpp.codeAnalysis.clangTidy.useBuildPath": false, + "C_Cpp.codeAnalysis.clangTidy.config": "", "[cpp]": { "editor.defaultFormatter": "ms-vscode.cpptools" }, @@ -38,6 +47,6 @@ "editor.defaultFormatter": "ms-vscode.cpptools" }, "[markdown]": { - "editor.defaultFormatter": "vscode.markdown-language-features" + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..2a11843 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,70 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Python: Run Tests", + "type": "shell", + "command": "python3", + "args": ["-m", "pytest", "tests/", "-v"], + "group": "test", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "Rust: Run Tests", + "type": "shell", + "command": "cargo", + "args": ["test"], + "options": { + "cwd": "${workspaceFolder}/native/rust" + }, + "group": "test", + "problemMatcher": "$rustc", + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "C++: Build Tests", + "type": "shell", + "command": "cmake", + "args": ["--build", "${workspaceFolder}/build/native-cpp", "--target", "cli_template_native_cpp_tests"], + "group": "build", + "problemMatcher": "$gcc", + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "C++: Run Tests", + "type": "shell", + "command": "${workspaceFolder}/build/native-cpp/cli_template_native_cpp_tests", + "group": "test", + "dependsOn": "C++: Build Tests", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "All: Run All Tests", + "dependsOn": [ + "Python: Run Tests", + "Rust: Run Tests", + "C++: Run Tests" + ], + "dependsOrder": "sequence", + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [] + } + ] +} From 8def62f86f7bea493b6c18601b45905af122a2e4 Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:02 +0000 Subject: [PATCH 06/18] ci: update GitHub workflows and templates --- .github/ISSUE_TEMPLATE/feature_request.md | 1 - .github/PULL_REQUEST_TEMPLATE.md | 34 +++++++++++++++++++---- .github/workflows/ci.yml | 31 +++++++++++++++++++-- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 1da7962..740449a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -11,4 +11,3 @@ What problem are you trying to solve? ## Proposal What would you like to see added/changed? - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3295e83..74a0b86 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,32 @@ ## Summary -Describe what this PR changes and why. +- What does this change and why? Link issue/ADR when possible. +- If breaking, include migration steps. -## Checklist +## Change Type -- [ ] Tests added/updated (if applicable) -- [ ] Docs updated (if applicable) -- [ ] `pre-commit run --all-files` passes -- [ ] `cargo test` passes +- [ ] Feature +- [ ] Bug fix +- [ ] Docs/Chore/Build +- [ ] Performance +- [ ] Breaking change (summarize migration above) + +## Impacted Surface + +- SDK/API: list new/changed/removed methods, params, responses. +- CLI: commands touched, expected output/exit code changes, help/error text updates. + +## Testing + +- [ ] Added/updated tests +- [ ] `pre-commit run --all-files` +- [ ] Python: `pytest` +- [ ] Rust: `cargo test --manifest-path native/rust/Cargo.toml` (if touched) +- [ ] C++: `ctest` (if touched) +- [ ] Manual CLI check(s): key command + output + +## Documentation + +- [ ] README/docs/examples updated +- [ ] CHANGELOG entry under Unreleased for user-facing change +- [ ] SDK docs/API reference updated if surface changed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8cd469..d8effc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,5 +72,32 @@ jobs: - name: Build run: cmake --build build/native-cpp - - name: Smoke test - run: ./build/native-cpp/cli-template-native-cpp hello alice + - name: Test + run: ctest --test-dir build/native-cpp --output-on-failure + + python: + runs-on: ubuntu-latest + needs: pre-commit + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Install dependencies + run: uv pip install --system -e ".[dev]" pytest-cov + + - name: Test with coverage + run: pytest --cov=cli_template --cov-report=xml --cov-report=term + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml + fail_ci_if_error: false From e15bd8d02e072c36018986b30ec61ff89eb5b371 Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:06 +0000 Subject: [PATCH 07/18] build: rename package to sdk-template and update dependencies --- pyproject.toml | 40 ++- uv.lock | 775 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 807 insertions(+), 8 deletions(-) create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index 5a837cb..047a74d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,25 +3,29 @@ requires = ["hatchling>=1.26.0"] build-backend = "hatchling.build" [project] -name = "cli-template" +name = "sdk-template" dynamic = ["version"] -description = "Python-first CLI template with optional Rust/C++ backends." +description = "Multilingual SDK/CLI template with Rust/C++ backends for performance-bound tasks." readme = "README.md" requires-python = ">=3.11" dependencies = [] +license = { file = "LICENSE" } [project.optional-dependencies] dev = [ "commitizen>=4.0.0", "pre-commit>=3.8.0", - "ruff>=0.7.0" + "pytest>=8.0.0", + "pytest-cov>=4.0.0", + "ruff>=0.7.0", + "pyright>=1.1.390", ] [project.scripts] -cli-template = "cli_template.cli:main" +sdk-template = "sdk_template.cli:main" [tool.hatch.version] -path = "src/cli_template/__init__.py" +path = "src/sdk_template/__init__.py" [tool.ruff] line-length = 100 @@ -29,7 +33,17 @@ target-version = "py311" extend-exclude = ["build", "native/rust/target"] [tool.ruff.lint] -select = ["E", "F", "I", "B", "UP"] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "B", # flake8-bugbear + "UP", # pyupgrade + "I", # import sorting + "C4", # flake8-comprehensions + "SIM", # flake8-simplify + "RUF", # Ruff-specific (e.g., unused noqa) +] [tool.commitizen] name = "cz_conventional_commits" @@ -39,6 +53,16 @@ tag_format = "v$version" update_changelog_on_bump = true changelog_file = "CHANGELOG.md" version_files = [ - "src/cli_template/__init__.py:__version__", - "native/rust/Cargo.toml:version" + "src/sdk_template/__init__.py:__version__", + "native/rust/Cargo.toml:version", ] + +[tool.pyright] +typeCheckingMode = "standard" # balanced defaults, not strict +include = ["src", "tests"] +exclude = ["build", "dist", ".venv", ".eggs", "native", "htmlcov"] +reportMissingTypeStubs = "warning" +reportUnknownParameterType = "warning" +reportUnknownArgumentType = "warning" +reportUnknownVariableType = "warning" +reportUntypedFunctionDecorator = "warning" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fd83ff3 --- /dev/null +++ b/uv.lock @@ -0,0 +1,775 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "commitizen" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "charset-normalizer" }, + { name = "colorama" }, + { name = "decli" }, + { name = "deprecated" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "prompt-toolkit" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "termcolor" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/bc/cb84a2d87b565964a30c7cdda2f0b467ddecba6953ab868f971560d93d5b/commitizen-4.10.1.tar.gz", hash = "sha256:14d12252970463db2fa7c7e7e4753321190a093e7d5c99efcd1a63be73e3c1f8", size = 57706, upload-time = "2025-12-11T15:25:05.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/b9/87d724956c29a3815befdb5beade54cc088c6a54a8475a0b5571fe2ef2ba/commitizen-4.10.1-py3-none-any.whl", hash = "sha256:ed4a377beed63aa4438f7ad5db791f66e117a5f597677a58b27a1c31e9f64fc4", size = 82588, upload-time = "2025-12-11T15:25:03.97Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, + { url = "https://files.pythonhosted.org/packages/0a/32/2e2f96e9d5691eaf1181d9040f850b8b7ce165ea10810fd8e2afa534cef7/coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", size = 250925, upload-time = "2025-12-08T13:12:37.221Z" }, + { url = "https://files.pythonhosted.org/packages/7b/45/b88ddac1d7978859b9a39a8a50ab323186148f1d64bc068f86fc77706321/coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", size = 253032, upload-time = "2025-12-08T13:12:38.763Z" }, + { url = "https://files.pythonhosted.org/packages/71/cb/e15513f94c69d4820a34b6bf3d2b1f9f8755fa6021be97c7065442d7d653/coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", size = 249134, upload-time = "2025-12-08T13:12:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/09/61/d960ff7dc9e902af3310ce632a875aaa7860f36d2bc8fc8b37ee7c1b82a5/coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", size = 250731, upload-time = "2025-12-08T13:12:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/98/34/c7c72821794afc7c7c2da1db8f00c2c98353078aa7fb6b5ff36aac834b52/coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", size = 248795, upload-time = "2025-12-08T13:12:43.331Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/e0f07107987a43b2def9aa041c614ddb38064cbf294a71ef8c67d43a0cdd/coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", size = 248514, upload-time = "2025-12-08T13:12:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/71/c2/c949c5d3b5e9fc6dd79e1b73cdb86a59ef14f3709b1d72bf7668ae12e000/coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", size = 249424, upload-time = "2025-12-08T13:12:45.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/f1/bbc009abd6537cec0dffb2cc08c17a7f03de74c970e6302db4342a6e05af/coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", size = 220597, upload-time = "2025-12-08T13:12:47.378Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/d9977f2fb51c10fbaed0718ce3d0a8541185290b981f73b1d27276c12d91/coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", size = 221536, upload-time = "2025-12-08T13:12:48.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/3fcf43fd96fb43e337a3073dea63ff148dcc5c41ba7a14d4c7d34efb2216/coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", size = 220206, upload-time = "2025-12-08T13:12:50.365Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, + { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, + { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, + { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "decli" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/59/d4ffff1dee2c8f6f2dd8f87010962e60f7b7847504d765c91ede5a466730/decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656", size = 7564, upload-time = "2025-06-01T15:23:41.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3", size = 7989, upload-time = "2025-06-01T15:23:40.228Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.407" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "questionary" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +] + +[[package]] +name = "sdk-template" +source = { editable = "." } + +[package.optional-dependencies] +dev = [ + { name = "commitizen" }, + { name = "pre-commit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "commitizen", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.8.0" }, + { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.390" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "termcolor" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/56/ab275c2b56a5e2342568838f0d5e3e66a32354adcc159b495e374cda43f5/termcolor-3.2.0.tar.gz", hash = "sha256:610e6456feec42c4bcd28934a8c87a06c3fa28b01561d46aa09a9881b8622c58", size = 14423, upload-time = "2025-10-25T19:11:42.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/d5/141f53d7c1eb2a80e6d3e9a390228c3222c27705cbe7f048d3623053f3ca/termcolor-3.2.0-py3-none-any.whl", hash = "sha256:a10343879eba4da819353c55cb8049b0933890c2ebf9ad5d3ecd2bb32ea96ea6", size = 7698, upload-time = "2025-10-25T19:11:41.536Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "wrapt" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" }, + { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" }, + { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" }, + { url = "https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" }, + { url = "https://files.pythonhosted.org/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" }, + { url = "https://files.pythonhosted.org/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" }, + { url = "https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, + { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" }, + { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" }, + { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +] From 8f6ddd34aa3dfd2e84f1a4ef12bd2ae32814a8db Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:11 +0000 Subject: [PATCH 08/18] refactor: remove old cli_template module --- src/cli_template/__init__.py | 3 -- src/cli_template/__main__.py | 6 --- src/cli_template/_native.py | 64 ---------------------- src/cli_template/cli.py | 102 ----------------------------------- 4 files changed, 175 deletions(-) delete mode 100644 src/cli_template/__init__.py delete mode 100644 src/cli_template/__main__.py delete mode 100644 src/cli_template/_native.py delete mode 100644 src/cli_template/cli.py diff --git a/src/cli_template/__init__.py b/src/cli_template/__init__.py deleted file mode 100644 index a05eb9a..0000000 --- a/src/cli_template/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = ["__version__"] - -__version__ = "0.1.0" diff --git a/src/cli_template/__main__.py b/src/cli_template/__main__.py deleted file mode 100644 index 855590e..0000000 --- a/src/cli_template/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -from __future__ import annotations - -from .cli import main - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/cli_template/_native.py b/src/cli_template/_native.py deleted file mode 100644 index 0551e19..0000000 --- a/src/cli_template/_native.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import annotations - -import os -import subprocess -import sys -from collections.abc import Sequence -from pathlib import Path - - -class NativeBackendError(RuntimeError): - pass - - -def _repo_root() -> Path: - env_root = os.environ.get("CLI_TEMPLATE_REPO_ROOT") - if env_root: - return Path(env_root).expanduser().resolve() - - cwd = Path.cwd() - for candidate in (cwd, *cwd.parents): - if (candidate / "pyproject.toml").is_file(): - return candidate - - raise NativeBackendError( - "Could not locate repo root (pyproject.toml). " - "Run from inside the repo or set CLI_TEMPLATE_REPO_ROOT." - ) - - -def _rust_bin_path(repo_root: Path) -> Path: - return repo_root / "native" / "rust" / "target" / "release" / "cli-template-native-rs" - - -def _cpp_bin_path(repo_root: Path) -> Path: - return repo_root / "build" / "native-cpp" / "cli-template-native-cpp" - - -def run_rust(argv: Sequence[str]) -> int: - repo_root = _repo_root() - binary = _rust_bin_path(repo_root) - if not binary.is_file(): - raise NativeBackendError( - f"Rust backend binary not found at '{binary}'. " - "Build it with: cargo build --manifest-path native/rust/Cargo.toml --release" - ) - result = subprocess.run([str(binary), *argv], check=False) - return result.returncode - - -def run_cpp(argv: Sequence[str]) -> int: - repo_root = _repo_root() - binary = _cpp_bin_path(repo_root) - if sys.platform == "win32": - binary = binary.with_suffix(".exe") - - if not binary.is_file(): - raise NativeBackendError( - f"C++ backend binary not found at '{binary}'. " - "Build it with: cmake -S native/cpp -B build/native-cpp -GNinja " - "-DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON " - "&& cmake --build build/native-cpp" - ) - result = subprocess.run([str(binary), *argv], check=False) - return result.returncode diff --git a/src/cli_template/cli.py b/src/cli_template/cli.py deleted file mode 100644 index 80734ca..0000000 --- a/src/cli_template/cli.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import annotations - -import argparse - -from ._native import NativeBackendError, run_cpp, run_rust -from . import __version__ - - -def _build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(prog="cli-template", description="Python-first CLI template.") - - parser.add_argument( - "--impl", - choices=("python", "rust", "cpp"), - default="python", - help="Implementation backend for commands that support native backends.", - ) - - sub = parser.add_subparsers(dest="command", required=True) - - hello = sub.add_parser("hello", help="Print a friendly greeting.") - hello.add_argument("--name", default="world") - - echo = sub.add_parser("echo", help="Echo the message.") - echo.add_argument("message", nargs="+") - - sum_cmd = sub.add_parser("sum", help="Print the sum of two integers.") - sum_cmd.add_argument("a", type=int) - sum_cmd.add_argument("b", type=int) - - sub.add_parser("info", help="Print backend info.") - - return parser - - -def main(argv: list[str] | None = None) -> int: - parser = _build_parser() - args = parser.parse_args(argv) - - if args.command == "hello": - if args.impl == "python": - print(f"hello, {args.name}") - return 0 - if args.impl == "rust": - try: - return run_rust(["hello", args.name]) - except NativeBackendError as exc: - parser.error(str(exc)) - if args.impl == "cpp": - try: - return run_cpp(["hello", args.name]) - except NativeBackendError as exc: - parser.error(str(exc)) - - if args.command == "echo": - if args.impl == "python": - print(" ".join(args.message)) - return 0 - if args.impl == "rust": - try: - return run_rust(["echo", *args.message]) - except NativeBackendError as exc: - parser.error(str(exc)) - if args.impl == "cpp": - try: - return run_cpp(["echo", *args.message]) - except NativeBackendError as exc: - parser.error(str(exc)) - - if args.command == "sum": - if args.impl == "python": - print(args.a + args.b) - return 0 - if args.impl == "rust": - try: - return run_rust(["sum", str(args.a), str(args.b)]) - except NativeBackendError as exc: - parser.error(str(exc)) - if args.impl == "cpp": - try: - return run_cpp(["sum", str(args.a), str(args.b)]) - except NativeBackendError as exc: - parser.error(str(exc)) - - if args.command == "info": - if args.impl == "python": - print("backend=python") - print(f"version={__version__}") - return 0 - if args.impl == "rust": - try: - return run_rust(["info"]) - except NativeBackendError as exc: - parser.error(str(exc)) - if args.impl == "cpp": - try: - return run_cpp(["info"]) - except NativeBackendError as exc: - parser.error(str(exc)) - - parser.error(f"unknown command: {args.command}") - return 2 From 726fd185ca7c9116eae3f2752b2f25af35532fc1 Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:17 +0000 Subject: [PATCH 09/18] feat: add sdk_template Python module with CLI and native bridge --- src/sdk_template/__init__.py | 3 ++ src/sdk_template/__main__.py | 6 +++ src/sdk_template/_native.py | 64 ++++++++++++++++++++++ src/sdk_template/cli.py | 102 +++++++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 src/sdk_template/__init__.py create mode 100644 src/sdk_template/__main__.py create mode 100644 src/sdk_template/_native.py create mode 100644 src/sdk_template/cli.py diff --git a/src/sdk_template/__init__.py b/src/sdk_template/__init__.py new file mode 100644 index 0000000..a05eb9a --- /dev/null +++ b/src/sdk_template/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["__version__"] + +__version__ = "0.1.0" diff --git a/src/sdk_template/__main__.py b/src/sdk_template/__main__.py new file mode 100644 index 0000000..00672e2 --- /dev/null +++ b/src/sdk_template/__main__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from .cli import main + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/src/sdk_template/_native.py b/src/sdk_template/_native.py new file mode 100644 index 0000000..a62ca67 --- /dev/null +++ b/src/sdk_template/_native.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import os +import subprocess +import sys +from collections.abc import Sequence +from pathlib import Path + + +class NativeBackendError(RuntimeError): + pass + + +def _repo_root() -> Path: + env_root = os.environ.get("SDK_TEMPLATE_REPO_ROOT") + if env_root: + return Path(env_root).expanduser().resolve() + + cwd = Path.cwd() + for candidate in (cwd, *cwd.parents): + if (candidate / "pyproject.toml").is_file(): + return candidate + + raise NativeBackendError( + "Could not locate repo root (pyproject.toml). " + "Run from inside the repo or set SDK_TEMPLATE_REPO_ROOT." + ) + + +def _rust_bin_path(repo_root: Path) -> Path: + return repo_root / "native" / "rust" / "target" / "release" / "cli-template-native-rs" + + +def _cpp_bin_path(repo_root: Path) -> Path: + return repo_root / "build" / "native-cpp" / "cli-template-native-cpp" + + +def run_rust(argv: Sequence[str]) -> int: + repo_root = _repo_root() + binary = _rust_bin_path(repo_root) + if not binary.is_file(): + raise NativeBackendError( + f"Rust backend binary not found at '{binary}'. " + "Build it with: cargo build --manifest-path native/rust/Cargo.toml --release" + ) + result = subprocess.run([str(binary), *argv], check=False) + return result.returncode + + +def run_cpp(argv: Sequence[str]) -> int: + repo_root = _repo_root() + binary = _cpp_bin_path(repo_root) + if sys.platform == "win32": + binary = binary.with_suffix(".exe") + + if not binary.is_file(): + raise NativeBackendError( + f"C++ backend binary not found at '{binary}'. " + "Build it with: cmake -S native/cpp -B build/native-cpp -GNinja " + "-DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON " + "&& cmake --build build/native-cpp" + ) + result = subprocess.run([str(binary), *argv], check=False) + return result.returncode diff --git a/src/sdk_template/cli.py b/src/sdk_template/cli.py new file mode 100644 index 0000000..b04e4db --- /dev/null +++ b/src/sdk_template/cli.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import argparse + +from . import __version__ +from ._native import NativeBackendError, run_cpp, run_rust + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="cli-template", description="Python-first CLI template.") + + parser.add_argument( + "--impl", + choices=("python", "rust", "cpp"), + default="python", + help="Implementation backend for commands that support native backends.", + ) + + sub = parser.add_subparsers(dest="command", required=True) + + hello = sub.add_parser("hello", help="Print a friendly greeting.") + hello.add_argument("--name", default="world") + + echo = sub.add_parser("echo", help="Echo the message.") + echo.add_argument("message", nargs="+") + + sum_cmd = sub.add_parser("sum", help="Print the sum of two integers.") + sum_cmd.add_argument("a", type=int) + sum_cmd.add_argument("b", type=int) + + sub.add_parser("info", help="Print backend info.") + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = _build_parser() + args = parser.parse_args(argv) + + if args.command == "hello": + if args.impl == "python": + print(f"hello, {args.name}") + return 0 + if args.impl == "rust": + try: + return run_rust(["hello", args.name]) + except NativeBackendError as exc: + parser.error(str(exc)) + if args.impl == "cpp": + try: + return run_cpp(["hello", args.name]) + except NativeBackendError as exc: + parser.error(str(exc)) + + if args.command == "echo": + if args.impl == "python": + print(" ".join(args.message)) + return 0 + if args.impl == "rust": + try: + return run_rust(["echo", *args.message]) + except NativeBackendError as exc: + parser.error(str(exc)) + if args.impl == "cpp": + try: + return run_cpp(["echo", *args.message]) + except NativeBackendError as exc: + parser.error(str(exc)) + + if args.command == "sum": + if args.impl == "python": + print(args.a + args.b) + return 0 + if args.impl == "rust": + try: + return run_rust(["sum", str(args.a), str(args.b)]) + except NativeBackendError as exc: + parser.error(str(exc)) + if args.impl == "cpp": + try: + return run_cpp(["sum", str(args.a), str(args.b)]) + except NativeBackendError as exc: + parser.error(str(exc)) + + if args.command == "info": + if args.impl == "python": + print("backend=python") + print(f"version={__version__}") + return 0 + if args.impl == "rust": + try: + return run_rust(["info"]) + except NativeBackendError as exc: + parser.error(str(exc)) + if args.impl == "cpp": + try: + return run_cpp(["info"]) + except NativeBackendError as exc: + parser.error(str(exc)) + + # This point is unreachable because argparse validates commands with required=True + return 0 # pragma: no cover From 1b2708c5c5dc7add63a4bd0988af87ff19ef9dec Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:21 +0000 Subject: [PATCH 10/18] test: add comprehensive test suite for Python CLI --- tests/README.md | 92 +++++++++- tests/test_cli.py | 449 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 538 insertions(+), 3 deletions(-) create mode 100644 tests/test_cli.py diff --git a/tests/README.md b/tests/README.md index 67459d6..334da72 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,93 @@ # Tests -Project tests live here. +This project maintains **100% code coverage** across all three languages. -- Rust integration tests: `tests/*.rs` -- Python tests: `pytest` tests can also live here. +## Running Tests +### Python + +```bash +# Run all Python tests +pytest + +# Run with verbose output +pytest -v +``` + +### Rust + +```bash +# Run all Rust tests +cargo test --manifest-path native/rust/Cargo.toml + +# Run with output shown +cargo test --manifest-path native/rust/Cargo.toml -- --nocapture +``` + +### C++ + +```bash +# Build with tests +cmake -S native/cpp -B build/native-cpp -GNinja -DCMAKE_BUILD_TYPE=Debug +cmake --build build/native-cpp + +# Run tests +ctest --test-dir build/native-cpp --output-on-failure + +# Or run directly +./build/native-cpp/cli_template_native_cpp_tests +``` + +## Code Coverage + +### Python (100%) + +```bash +# Run with coverage report +pytest --cov=sdk_template --cov-report=term-missing + +# Generate HTML report +pytest --cov=sdk_template --cov-report=html:build/coverage-html +# Open build/coverage-html/index.html +``` + +### Rust (100%) + +```bash +# Install coverage tool (first time - done automatically in devcontainer) +rustup component add llvm-tools-preview +cargo install cargo-llvm-cov + +# Run library tests with coverage (excludes main.rs binary entry point) +cargo llvm-cov --manifest-path native/rust/Cargo.toml --lib + +# Generate HTML report +cargo llvm-cov --manifest-path native/rust/Cargo.toml --lib --html --output-dir build/coverage-rust +``` + +### C++ (100%) + +```bash +# Build with coverage enabled +cmake -S native/cpp -B build/coverage -GNinja -DCMAKE_BUILD_TYPE=Debug -DCOVERAGE=ON +cmake --build build/coverage + +# Run tests (generates .gcda files) +./build/coverage/cli_template_native_cpp_tests + +# Generate report with lcov (excludes main.cpp and test files) +lcov --capture --directory build/coverage --output-file build/coverage/coverage.info +lcov --remove build/coverage/coverage.info '/usr/*' '*/tests/*' --output-file build/coverage/coverage_filtered.info +lcov --list build/coverage/coverage_filtered.info + +# Generate HTML report +genhtml build/coverage/coverage_filtered.info --output-directory build/coverage-cpp +``` + +## Run All Tests + +```bash +pytest && \ +cargo test --manifest-path native/rust/Cargo.toml && \ +ctest --test-dir build/native-cpp --output-on-failure +``` diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..62226c6 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,449 @@ +"""Unit tests for the Python CLI.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path +from unittest import mock + +import pytest + +from sdk_template import __version__ +from sdk_template._native import ( + NativeBackendError, + _cpp_bin_path, + _repo_root, + _rust_bin_path, + run_cpp, + run_rust, +) +from sdk_template.cli import main + + +class TestHelloCommand: + """Tests for the hello command.""" + + def test_hello_default_name(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test hello with default name.""" + result = main(["hello"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "hello, world\n" + + def test_hello_custom_name(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test hello with custom name.""" + result = main(["hello", "--name", "alice"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "hello, alice\n" + + def test_hello_empty_name(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test hello with empty name.""" + result = main(["hello", "--name", ""]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "hello, \n" + + def test_hello_special_characters(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test hello with special characters in name.""" + result = main(["hello", "--name", "Bob!@#"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "hello, Bob!@#\n" + + +class TestEchoCommand: + """Tests for the echo command.""" + + def test_echo_single_word(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test echo with single word.""" + result = main(["echo", "hello"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "hello\n" + + def test_echo_multiple_words(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test echo with multiple words.""" + result = main(["echo", "hello", "world"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "hello world\n" + + def test_echo_many_words(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test echo with many words.""" + result = main(["echo", "one", "two", "three", "four", "five"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "one two three four five\n" + + def test_echo_special_characters(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test echo with special characters.""" + result = main(["echo", "hello!@#$%"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "hello!@#$%\n" + + +class TestSumCommand: + """Tests for the sum command.""" + + def test_sum_positive_numbers(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test sum of two positive numbers.""" + result = main(["sum", "2", "3"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "5\n" + + def test_sum_negative_numbers(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test sum with negative numbers.""" + result = main(["sum", "-5", "3"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "-2\n" + + def test_sum_both_negative(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test sum with both negative numbers.""" + result = main(["sum", "-5", "-3"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "-8\n" + + def test_sum_zero(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test sum with zero.""" + result = main(["sum", "0", "0"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "0\n" + + def test_sum_large_numbers(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test sum with large numbers.""" + result = main(["sum", "1000000", "2000000"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "3000000\n" + + +class TestInfoCommand: + """Tests for the info command.""" + + def test_info_python_backend(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test info command shows python backend.""" + result = main(["info"]) + assert result == 0 + captured = capsys.readouterr() + assert "backend=python" in captured.out + assert f"version={__version__}" in captured.out + + +class TestImplFlag: + """Tests for the --impl flag.""" + + def test_impl_python_is_default(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test that python is the default implementation.""" + result = main(["--impl", "python", "hello"]) + assert result == 0 + captured = capsys.readouterr() + assert captured.out == "hello, world\n" + + def test_impl_rust_missing_binary(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test rust impl fails gracefully when binary missing.""" + # Point to a non-existent directory + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", "/nonexistent") + with pytest.raises(SystemExit) as exc_info: + main(["--impl", "rust", "hello"]) + assert exc_info.value.code == 2 + + def test_impl_cpp_missing_binary(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test cpp impl fails gracefully when binary missing.""" + # Point to a non-existent directory + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", "/nonexistent") + with pytest.raises(SystemExit) as exc_info: + main(["--impl", "cpp", "hello"]) + assert exc_info.value.code == 2 + + # Tests for successful native backend execution + def test_impl_rust_hello_success(self) -> None: + """Test rust impl hello succeeds when binary exists.""" + with mock.patch("cli_template.cli.run_rust") as mock_run: + mock_run.return_value = 0 + result = main(["--impl", "rust", "hello"]) + assert result == 0 + mock_run.assert_called_once_with(["hello", "world"]) + + def test_impl_cpp_hello_success(self) -> None: + """Test cpp impl hello succeeds when binary exists.""" + with mock.patch("cli_template.cli.run_cpp") as mock_run: + mock_run.return_value = 0 + result = main(["--impl", "cpp", "hello"]) + assert result == 0 + mock_run.assert_called_once_with(["hello", "world"]) + + def test_impl_rust_echo_success(self) -> None: + """Test rust impl echo succeeds when binary exists.""" + with mock.patch("cli_template.cli.run_rust") as mock_run: + mock_run.return_value = 0 + result = main(["--impl", "rust", "echo", "hello", "world"]) + assert result == 0 + mock_run.assert_called_once_with(["echo", "hello", "world"]) + + def test_impl_cpp_echo_success(self) -> None: + """Test cpp impl echo succeeds when binary exists.""" + with mock.patch("cli_template.cli.run_cpp") as mock_run: + mock_run.return_value = 0 + result = main(["--impl", "cpp", "echo", "hello", "world"]) + assert result == 0 + mock_run.assert_called_once_with(["echo", "hello", "world"]) + + def test_impl_rust_echo_error(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test rust impl echo fails gracefully when binary missing.""" + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", "/nonexistent") + with pytest.raises(SystemExit) as exc_info: + main(["--impl", "rust", "echo", "test"]) + assert exc_info.value.code == 2 + + def test_impl_cpp_echo_error(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test cpp impl echo fails gracefully when binary missing.""" + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", "/nonexistent") + with pytest.raises(SystemExit) as exc_info: + main(["--impl", "cpp", "echo", "test"]) + assert exc_info.value.code == 2 + + def test_impl_rust_sum_success(self) -> None: + """Test rust impl sum succeeds when binary exists.""" + with mock.patch("cli_template.cli.run_rust") as mock_run: + mock_run.return_value = 0 + result = main(["--impl", "rust", "sum", "1", "2"]) + assert result == 0 + mock_run.assert_called_once_with(["sum", "1", "2"]) + + def test_impl_cpp_sum_success(self) -> None: + """Test cpp impl sum succeeds when binary exists.""" + with mock.patch("cli_template.cli.run_cpp") as mock_run: + mock_run.return_value = 0 + result = main(["--impl", "cpp", "sum", "1", "2"]) + assert result == 0 + mock_run.assert_called_once_with(["sum", "1", "2"]) + + def test_impl_rust_sum_error(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test rust impl sum fails gracefully when binary missing.""" + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", "/nonexistent") + with pytest.raises(SystemExit) as exc_info: + main(["--impl", "rust", "sum", "1", "2"]) + assert exc_info.value.code == 2 + + def test_impl_cpp_sum_error(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test cpp impl sum fails gracefully when binary missing.""" + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", "/nonexistent") + with pytest.raises(SystemExit) as exc_info: + main(["--impl", "cpp", "sum", "1", "2"]) + assert exc_info.value.code == 2 + + def test_impl_rust_info_success(self) -> None: + """Test rust impl info succeeds when binary exists.""" + with mock.patch("cli_template.cli.run_rust") as mock_run: + mock_run.return_value = 0 + result = main(["--impl", "rust", "info"]) + assert result == 0 + mock_run.assert_called_once_with(["info"]) + + def test_impl_cpp_info_success(self) -> None: + """Test cpp impl info succeeds when binary exists.""" + with mock.patch("cli_template.cli.run_cpp") as mock_run: + mock_run.return_value = 0 + result = main(["--impl", "cpp", "info"]) + assert result == 0 + mock_run.assert_called_once_with(["info"]) + + def test_impl_rust_info_error(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test rust impl info fails gracefully when binary missing.""" + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", "/nonexistent") + with pytest.raises(SystemExit) as exc_info: + main(["--impl", "rust", "info"]) + assert exc_info.value.code == 2 + + def test_impl_cpp_info_error(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test cpp impl info fails gracefully when binary missing.""" + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", "/nonexistent") + with pytest.raises(SystemExit) as exc_info: + main(["--impl", "cpp", "info"]) + assert exc_info.value.code == 2 + + +class TestInvalidInput: + """Tests for invalid input handling.""" + + def test_missing_command(self) -> None: + """Test that missing command exits with error.""" + with pytest.raises(SystemExit) as exc_info: + main([]) + assert exc_info.value.code == 2 + + def test_invalid_command(self) -> None: + """Test that invalid command exits with error.""" + with pytest.raises(SystemExit) as exc_info: + main(["invalid_command"]) + assert exc_info.value.code == 2 + + def test_invalid_sum_argument(self) -> None: + """Test that invalid sum argument exits with error.""" + with pytest.raises(SystemExit) as exc_info: + main(["sum", "not_a_number", "3"]) + assert exc_info.value.code == 2 + + def test_invalid_impl_choice(self) -> None: + """Test that invalid impl choice exits with error.""" + with pytest.raises(SystemExit) as exc_info: + main(["--impl", "invalid", "hello"]) + assert exc_info.value.code == 2 + + +class TestNativeBackend: + """Tests for native backend utilities.""" + + def test_repo_root_found(self) -> None: + """Test that repo root can be found.""" + root = _repo_root() + assert (root / "pyproject.toml").is_file() + + def test_repo_root_from_env(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test repo root from environment variable.""" + # Create a fake pyproject.toml + fake_root = tmp_path / "fake_project" + fake_root.mkdir() + (fake_root / "pyproject.toml").write_text("[project]\nname = 'test'\n") + + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", str(fake_root)) + root = _repo_root() + assert root == fake_root + + def test_repo_root_not_found(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that repo root raises error when not found.""" + # Change to a directory without pyproject.toml and clear env var + monkeypatch.delenv("CLI_TEMPLATE_REPO_ROOT", raising=False) + monkeypatch.chdir(tmp_path) + with pytest.raises(NativeBackendError, match="Could not locate repo root"): + _repo_root() + + def test_native_backend_error(self) -> None: + """Test NativeBackendError can be raised.""" + with pytest.raises(NativeBackendError): + raise NativeBackendError("test error") + + def test_rust_bin_path(self) -> None: + """Test Rust binary path construction.""" + root = Path("/fake/root") + path = _rust_bin_path(root) + assert path == root / "native" / "rust" / "target" / "release" / "cli-template-native-rs" + + def test_cpp_bin_path(self) -> None: + """Test C++ binary path construction.""" + root = Path("/fake/root") + path = _cpp_bin_path(root) + assert path == root / "build" / "native-cpp" / "cli-template-native-cpp" + + def test_run_rust_success(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test successful Rust binary execution.""" + # Create a fake binary + fake_root = tmp_path / "fake_project" + fake_root.mkdir(parents=True) + (fake_root / "pyproject.toml").write_text("[project]\nname = 'test'\n") + + binary_dir = fake_root / "native" / "rust" / "target" / "release" + binary_dir.mkdir(parents=True) + binary = binary_dir / "cli-template-native-rs" + binary.write_text("#!/bin/sh\necho 'fake rust'\n") + binary.chmod(0o755) + + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", str(fake_root)) + + # Mock subprocess.run to avoid actually running anything + with mock.patch("cli_template._native.subprocess.run") as mock_run: + mock_run.return_value = mock.Mock(returncode=0) + result = run_rust(["hello"]) + assert result == 0 + mock_run.assert_called_once() + + def test_run_cpp_success(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test successful C++ binary execution.""" + # Create a fake binary + fake_root = tmp_path / "fake_project" + fake_root.mkdir(parents=True) + (fake_root / "pyproject.toml").write_text("[project]\nname = 'test'\n") + + binary_dir = fake_root / "build" / "native-cpp" + binary_dir.mkdir(parents=True) + binary = binary_dir / "cli-template-native-cpp" + binary.write_text("#!/bin/sh\necho 'fake cpp'\n") + binary.chmod(0o755) + + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", str(fake_root)) + + # Mock subprocess.run to avoid actually running anything + with mock.patch("cli_template._native.subprocess.run") as mock_run: + mock_run.return_value = mock.Mock(returncode=0) + result = run_cpp(["hello"]) + assert result == 0 + mock_run.assert_called_once() + + def test_run_cpp_windows_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test C++ binary path has .exe suffix on Windows.""" + # Create a fake binary with .exe suffix + fake_root = tmp_path / "fake_project" + fake_root.mkdir(parents=True) + (fake_root / "pyproject.toml").write_text("[project]\nname = 'test'\n") + + binary_dir = fake_root / "build" / "native-cpp" + binary_dir.mkdir(parents=True) + binary = binary_dir / "cli-template-native-cpp.exe" + binary.write_text("fake windows exe") + + monkeypatch.setenv("CLI_TEMPLATE_REPO_ROOT", str(fake_root)) + monkeypatch.setattr("cli_template._native.sys.platform", "win32") + + # Mock subprocess.run to avoid actually running anything + with mock.patch("cli_template._native.subprocess.run") as mock_run: + mock_run.return_value = mock.Mock(returncode=0) + result = run_cpp(["hello"]) + assert result == 0 + # Verify the .exe suffix was used + call_args = mock_run.call_args[0][0] + assert call_args[0].endswith(".exe") + + +class TestMainModule: + """Tests for __main__.py entry point.""" + + def test_module_execution(self) -> None: + """Test running as python -m cli_template.""" + result = subprocess.run( + [sys.executable, "-m", "cli_template", "hello", "--name", "test"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + assert "hello, test" in result.stdout + + def test_main_module_import(self) -> None: + """Test that __main__.py can be imported and runs main().""" + import cli_template.__main__ as main_module + + # Verify the module imports main from cli + assert hasattr(main_module, "main") + + # Test that main is callable + with mock.patch.object(main_module, "main", return_value=0) as mock_main: + # The module-level code runs at import, so we test the imported function + assert callable(main_module.main) + + def test_main_module_direct_execution(self, capsys: pytest.CaptureFixture[str]) -> None: + """Test the main() function directly from __main__ module.""" + import cli_template.__main__ as main_module + + # Call main with args + with mock.patch("sys.argv", ["cli-template", "hello"]): + result = main_module.main() + assert result == 0 + + captured = capsys.readouterr() + assert "hello, world" in captured.out From ae6b22d73dd520583d7baa2872a2693ce9b87691 Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:25 +0000 Subject: [PATCH 11/18] feat: add Rust native backend with library and CLI --- native/rust/Cargo.toml | 9 ++ native/rust/src/lib.rs | 45 ++++++++ native/rust/src/main.rs | 41 +------- native/rust/src/tests.rs | 221 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 38 deletions(-) create mode 100644 native/rust/src/lib.rs create mode 100644 native/rust/src/tests.rs diff --git a/native/rust/Cargo.toml b/native/rust/Cargo.toml index d51ca89..c062f0e 100644 --- a/native/rust/Cargo.toml +++ b/native/rust/Cargo.toml @@ -3,5 +3,14 @@ name = "cli-template-native-rs" version = "0.1.0" edition = "2021" publish = false +license = "Apache-2.0" + +[lib] +name = "cli_template_native_rs" +path = "src/lib.rs" + +[[bin]] +name = "cli-template-native-rs" +path = "src/main.rs" [dependencies] diff --git a/native/rust/src/lib.rs b/native/rust/src/lib.rs new file mode 100644 index 0000000..71d8df9 --- /dev/null +++ b/native/rust/src/lib.rs @@ -0,0 +1,45 @@ +//! CLI Template Native Rust Backend +//! +//! This library provides the core functionality for the Rust native backend. + +pub mod echo; +pub mod hello; +pub mod info; +pub mod sum; + +#[cfg(test)] +mod tests; + +/// Print usage help to stderr. +pub fn print_help() { + eprintln!("cli-template-native-rs (native Rust backend)"); + eprintln!(); + eprintln!("Usage:"); + eprintln!(" cli-template-native-rs [args]"); + eprintln!(); + eprintln!("Commands:"); + eprintln!(" hello [name] Print a friendly greeting"); + eprintln!(" echo Echo the message"); + eprintln!(" sum Print the sum of two integers"); + eprintln!(" info Print backend info"); +} + +/// Dispatch a command and return the exit code. +pub fn dispatch(command: &str, rest: &[String]) -> i32 { + match command { + "-h" | "--help" | "help" => { + print_help(); + 0 + } + "hello" => hello::run(rest), + "echo" => echo::run(rest), + "sum" => sum::run(rest), + "info" => info::run(), + _ => { + eprintln!("unknown command: {command}"); + eprintln!(); + print_help(); + 2 + } + } +} diff --git a/native/rust/src/main.rs b/native/rust/src/main.rs index d9b045d..782852f 100644 --- a/native/rust/src/main.rs +++ b/native/rust/src/main.rs @@ -1,46 +1,11 @@ -mod echo; -mod hello; -mod info; -mod sum; - -fn print_help() { - eprintln!("cli-template-native-rs (native Rust backend)"); - eprintln!(); - eprintln!("Usage:"); - eprintln!(" cli-template-native-rs [args]"); - eprintln!(); - eprintln!("Commands:"); - eprintln!(" hello [name] Print a friendly greeting"); - eprintln!(" echo Echo the message"); - eprintln!(" sum Print the sum of two integers"); - eprintln!(" info Print backend info"); -} +//! CLI Template Native Rust Backend - Binary Entry Point fn main() { let mut args = std::env::args(); let _bin = args.next(); - let command = args - .next() - .unwrap_or_else(|| "--help".to_string()) - .to_string(); + let command = args.next().unwrap_or_else(|| "--help".to_string()); let rest: Vec = args.collect(); - let exit_code = match command.as_str() { - "-h" | "--help" | "help" => { - print_help(); - 0 - } - "hello" => hello::run(&rest), - "echo" => echo::run(&rest), - "sum" => sum::run(&rest), - "info" => info::run(), - _ => { - eprintln!("unknown command: {command}"); - eprintln!(); - print_help(); - 2 - } - }; - + let exit_code = cli_template_native_rs::dispatch(&command, &rest); std::process::exit(exit_code); } diff --git a/native/rust/src/tests.rs b/native/rust/src/tests.rs new file mode 100644 index 0000000..d6ad66f --- /dev/null +++ b/native/rust/src/tests.rs @@ -0,0 +1,221 @@ +#[cfg(test)] +mod tests { + use crate::echo; + use crate::hello; + use crate::info; + use crate::sum; + use crate::{dispatch, print_help}; + + // ==================== Hello Tests ==================== + + #[test] + fn test_hello_default_name() { + let args: Vec = vec![]; + let result = hello::run(&args); + assert_eq!(result, 0); + } + + #[test] + fn test_hello_custom_name() { + let args: Vec = vec!["alice".to_string()]; + let result = hello::run(&args); + assert_eq!(result, 0); + } + + #[test] + fn test_hello_name_with_spaces() { + let args: Vec = vec!["john doe".to_string()]; + let result = hello::run(&args); + assert_eq!(result, 0); + } + + #[test] + fn test_hello_empty_name() { + let args: Vec = vec!["".to_string()]; + let result = hello::run(&args); + assert_eq!(result, 0); + } + + // ==================== Echo Tests ==================== + + #[test] + fn test_echo_single_word() { + let args: Vec = vec!["hello".to_string()]; + let result = echo::run(&args); + assert_eq!(result, 0); + } + + #[test] + fn test_echo_multiple_words() { + let args: Vec = vec!["hello".to_string(), "world".to_string()]; + let result = echo::run(&args); + assert_eq!(result, 0); + } + + #[test] + fn test_echo_empty_args() { + let args: Vec = vec![]; + let result = echo::run(&args); + assert_eq!(result, 2); // Should fail with usage error + } + + #[test] + fn test_echo_special_characters() { + let args: Vec = vec!["hello!@#$%".to_string()]; + let result = echo::run(&args); + assert_eq!(result, 0); + } + + // ==================== Sum Tests ==================== + + #[test] + fn test_sum_positive_numbers() { + let args: Vec = vec!["2".to_string(), "3".to_string()]; + let result = sum::run(&args); + assert_eq!(result, 0); + } + + #[test] + fn test_sum_negative_numbers() { + let args: Vec = vec!["-5".to_string(), "3".to_string()]; + let result = sum::run(&args); + assert_eq!(result, 0); + } + + #[test] + fn test_sum_both_negative() { + let args: Vec = vec!["-5".to_string(), "-3".to_string()]; + let result = sum::run(&args); + assert_eq!(result, 0); + } + + #[test] + fn test_sum_zero() { + let args: Vec = vec!["0".to_string(), "0".to_string()]; + let result = sum::run(&args); + assert_eq!(result, 0); + } + + #[test] + fn test_sum_large_numbers() { + let args: Vec = vec!["1000000".to_string(), "2000000".to_string()]; + let result = sum::run(&args); + assert_eq!(result, 0); + } + + #[test] + fn test_sum_wrong_arg_count_none() { + let args: Vec = vec![]; + let result = sum::run(&args); + assert_eq!(result, 2); + } + + #[test] + fn test_sum_wrong_arg_count_one() { + let args: Vec = vec!["1".to_string()]; + let result = sum::run(&args); + assert_eq!(result, 2); + } + + #[test] + fn test_sum_wrong_arg_count_three() { + let args: Vec = vec!["1".to_string(), "2".to_string(), "3".to_string()]; + let result = sum::run(&args); + assert_eq!(result, 2); + } + + #[test] + fn test_sum_invalid_first_integer() { + let args: Vec = vec!["not_a_number".to_string(), "3".to_string()]; + let result = sum::run(&args); + assert_eq!(result, 2); + } + + #[test] + fn test_sum_invalid_second_integer() { + let args: Vec = vec!["3".to_string(), "not_a_number".to_string()]; + let result = sum::run(&args); + assert_eq!(result, 2); + } + + #[test] + fn test_sum_float_rejected() { + let args: Vec = vec!["3.14".to_string(), "2".to_string()]; + let result = sum::run(&args); + assert_eq!(result, 2); + } + + // ==================== Info Tests ==================== + + #[test] + fn test_info_returns_success() { + let result = info::run(); + assert_eq!(result, 0); + } + + // ==================== Dispatch Tests ==================== + + #[test] + fn test_dispatch_hello() { + let args: Vec = vec![]; + let result = dispatch("hello", &args); + assert_eq!(result, 0); + } + + #[test] + fn test_dispatch_echo() { + let args: Vec = vec!["test".to_string()]; + let result = dispatch("echo", &args); + assert_eq!(result, 0); + } + + #[test] + fn test_dispatch_sum() { + let args: Vec = vec!["1".to_string(), "2".to_string()]; + let result = dispatch("sum", &args); + assert_eq!(result, 0); + } + + #[test] + fn test_dispatch_info() { + let args: Vec = vec![]; + let result = dispatch("info", &args); + assert_eq!(result, 0); + } + + #[test] + fn test_dispatch_help_short() { + let args: Vec = vec![]; + let result = dispatch("-h", &args); + assert_eq!(result, 0); + } + + #[test] + fn test_dispatch_help_long() { + let args: Vec = vec![]; + let result = dispatch("--help", &args); + assert_eq!(result, 0); + } + + #[test] + fn test_dispatch_help_word() { + let args: Vec = vec![]; + let result = dispatch("help", &args); + assert_eq!(result, 0); + } + + #[test] + fn test_dispatch_unknown_command() { + let args: Vec = vec![]; + let result = dispatch("unknown", &args); + assert_eq!(result, 2); + } + + // ==================== Print Help Tests ==================== + + #[test] + fn test_print_help_does_not_panic() { + // Just ensure it doesn't panic + print_help(); + } +} From f16c351baabbb2371d29aba0bc10441821bf498e Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:30 +0000 Subject: [PATCH 12/18] feat: add C++ dispatch system and update implementations --- native/cpp/src/dispatch.cpp | 43 +++++++++++++++++++++++++++++++++++++ native/cpp/src/dispatch.h | 21 ++++++++++++++++++ native/cpp/src/main.cpp | 43 +++---------------------------------- native/cpp/src/sum.cpp | 5 ++--- 4 files changed, 69 insertions(+), 43 deletions(-) create mode 100644 native/cpp/src/dispatch.cpp create mode 100644 native/cpp/src/dispatch.h diff --git a/native/cpp/src/dispatch.cpp b/native/cpp/src/dispatch.cpp new file mode 100644 index 0000000..c5a0610 --- /dev/null +++ b/native/cpp/src/dispatch.cpp @@ -0,0 +1,43 @@ +#include "dispatch.h" +#include +#include + +int cmd_echo(int argc, char **argv); +int cmd_hello(int argc, char **argv); +int cmd_info(int argc, char **argv); +int cmd_sum(int argc, char **argv); + +void print_help() { + std::cerr << "cli-template-native-cpp (native C++ backend)\n\n"; + std::cerr << "Usage:\n"; + std::cerr << " cli-template-native-cpp [args]\n\n"; + std::cerr << "Commands:\n"; + std::cerr << " hello [name] Print a friendly greeting\n"; + std::cerr << " echo Echo the message\n"; + std::cerr << " sum Print the sum of two integers\n"; + std::cerr << " info Print backend info\n"; +} + +int dispatch(const std::string &command, int argc, char **argv) { + if (command == "-h" || command == "--help" || command == "help") { + print_help(); + return 0; + } + + if (command == "hello") { + return cmd_hello(argc, argv); + } + if (command == "echo") { + return cmd_echo(argc, argv); + } + if (command == "sum") { + return cmd_sum(argc, argv); + } + if (command == "info") { + return cmd_info(argc, argv); + } + + std::cerr << "unknown command: " << command << std::endl << std::endl; + print_help(); + return 2; +} diff --git a/native/cpp/src/dispatch.h b/native/cpp/src/dispatch.h new file mode 100644 index 0000000..a13a71b --- /dev/null +++ b/native/cpp/src/dispatch.h @@ -0,0 +1,21 @@ +#ifndef CLI_TEMPLATE_DISPATCH_H +#define CLI_TEMPLATE_DISPATCH_H + +#include + +/** + * Print usage help to stderr. + */ +void print_help(); + +/** + * Dispatch a command and return the exit code. + * + * @param command The command name (e.g., "hello", "echo", "sum", "info", "--help") + * @param argc Argument count (including the command name as argv[0]) + * @param argv Argument values + * @return Exit code (0 for success, 2 for error) + */ +int dispatch(const std::string &command, int argc, char **argv); + +#endif // CLI_TEMPLATE_DISPATCH_H diff --git a/native/cpp/src/main.cpp b/native/cpp/src/main.cpp index cf18bda..d370380 100644 --- a/native/cpp/src/main.cpp +++ b/native/cpp/src/main.cpp @@ -1,44 +1,7 @@ -#include +#include "dispatch.h" #include -int cmd_echo(int argc, char** argv); -int cmd_hello(int argc, char** argv); -int cmd_info(int argc, char** argv); -int cmd_sum(int argc, char** argv); - -static void print_help() { - std::cerr << "cli-template-native-cpp (native C++ backend)\n\n"; - std::cerr << "Usage:\n"; - std::cerr << " cli-template-native-cpp [args]\n\n"; - std::cerr << "Commands:\n"; - std::cerr << " hello [name] Print a friendly greeting\n"; - std::cerr << " echo Echo the message\n"; - std::cerr << " sum Print the sum of two integers\n"; - std::cerr << " info Print backend info\n"; -} - -int main(int argc, char** argv) { +int main(int argc, char **argv) { std::string command = argc > 1 ? argv[1] : "--help"; - - if (command == "-h" || command == "--help" || command == "help") { - print_help(); - return 0; - } - - if (command == "hello") { - return cmd_hello(argc - 1, argv + 1); - } - if (command == "echo") { - return cmd_echo(argc - 1, argv + 1); - } - if (command == "sum") { - return cmd_sum(argc - 1, argv + 1); - } - if (command == "info") { - return cmd_info(argc - 1, argv + 1); - } - - std::cerr << "unknown command: " << command << std::endl << std::endl; - print_help(); - return 2; + return dispatch(command, argc - 1, argv + 1); } diff --git a/native/cpp/src/sum.cpp b/native/cpp/src/sum.cpp index e53bab7..e09c45d 100644 --- a/native/cpp/src/sum.cpp +++ b/native/cpp/src/sum.cpp @@ -1,7 +1,7 @@ #include #include -static bool parse_i64(const std::string& value, long long* out) { +static bool parse_i64(const std::string &value, long long *out) { try { size_t pos = 0; long long parsed = std::stoll(value, &pos, 10); @@ -15,7 +15,7 @@ static bool parse_i64(const std::string& value, long long* out) { } } -int cmd_sum(int argc, char** argv) { +int cmd_sum(int argc, char **argv) { // argv[0] == "sum" if (argc != 3) { std::cerr << "usage: cli-template-native-cpp sum " << std::endl; @@ -36,4 +36,3 @@ int cmd_sum(int argc, char** argv) { std::cout << (a + b) << std::endl; return 0; } - From b476573cf83732d0b8b864a2b252587c3bf7844a Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:34 +0000 Subject: [PATCH 13/18] test: add C++ unit tests with Catch2 --- native/cpp/tests/test_functions.cpp | 323 ++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 native/cpp/tests/test_functions.cpp diff --git a/native/cpp/tests/test_functions.cpp b/native/cpp/tests/test_functions.cpp new file mode 100644 index 0000000..d3cbf2c --- /dev/null +++ b/native/cpp/tests/test_functions.cpp @@ -0,0 +1,323 @@ +/** + * Unit tests for C++ CLI functions. + * + * Uses a simple test framework without external dependencies. + */ + +#include +#include +#include +#include +#include +#include + +// Forward declarations of functions to test +int cmd_hello(int argc, char **argv); +int cmd_sum(int argc, char **argv); +int cmd_echo(int argc, char **argv); +int cmd_info(int argc, char **argv); +void print_help(); +int dispatch(const std::string &command, int argc, char **argv); + +// Test helpers +static int tests_run = 0; +static int tests_passed = 0; + +#define TEST(name) \ + void name(); \ + static bool name##_registered = []() { return true; }(); \ + void name() + +#define RUN_TEST(name) \ + do { \ + tests_run++; \ + std::cout << "Running " #name "... "; \ + try { \ + name(); \ + tests_passed++; \ + std::cout << "PASSED" << std::endl; \ + } catch (const std::exception &e) { \ + std::cout << "FAILED: " << e.what() << std::endl; \ + } \ + } while (0) + +#define ASSERT_EQ(a, b) \ + do { \ + if ((a) != (b)) { \ + throw std::runtime_error("Assertion failed: " #a " != " #b); \ + } \ + } while (0) + +// Helper to create argv from strings +static std::vector make_argv(std::vector &args) { + std::vector argv; + for (auto &arg : args) { + argv.push_back(arg.data()); + } + return argv; +} + +// ==================== Hello Tests ==================== + +TEST(test_hello_default_name) { + std::vector args = {"hello"}; + auto argv = make_argv(args); + int result = cmd_hello(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_hello_custom_name) { + std::vector args = {"hello", "alice"}; + auto argv = make_argv(args); + int result = cmd_hello(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_hello_name_with_spaces) { + std::vector args = {"hello", "john doe"}; + auto argv = make_argv(args); + int result = cmd_hello(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_hello_empty_name) { + std::vector args = {"hello", ""}; + auto argv = make_argv(args); + int result = cmd_hello(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +// ==================== Sum Tests ==================== + +TEST(test_sum_positive_numbers) { + std::vector args = {"sum", "2", "3"}; + auto argv = make_argv(args); + int result = cmd_sum(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_sum_negative_numbers) { + std::vector args = {"sum", "-5", "3"}; + auto argv = make_argv(args); + int result = cmd_sum(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_sum_both_negative) { + std::vector args = {"sum", "-5", "-3"}; + auto argv = make_argv(args); + int result = cmd_sum(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_sum_zero) { + std::vector args = {"sum", "0", "0"}; + auto argv = make_argv(args); + int result = cmd_sum(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_sum_large_numbers) { + std::vector args = {"sum", "1000000", "2000000"}; + auto argv = make_argv(args); + int result = cmd_sum(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_sum_wrong_arg_count_one) { + std::vector args = {"sum", "1"}; + auto argv = make_argv(args); + int result = cmd_sum(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 2); +} + +TEST(test_sum_wrong_arg_count_none) { + std::vector args = {"sum"}; + auto argv = make_argv(args); + int result = cmd_sum(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 2); +} + +TEST(test_sum_invalid_first_integer) { + std::vector args = {"sum", "not_a_number", "3"}; + auto argv = make_argv(args); + int result = cmd_sum(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 2); +} + +TEST(test_sum_invalid_second_integer) { + std::vector args = {"sum", "3", "not_a_number"}; + auto argv = make_argv(args); + int result = cmd_sum(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 2); +} + +TEST(test_sum_float_rejected) { + std::vector args = {"sum", "3.14", "2"}; + auto argv = make_argv(args); + int result = cmd_sum(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 2); +} + +// ==================== Echo Tests ==================== + +TEST(test_echo_single_word) { + std::vector args = {"echo", "hello"}; + auto argv = make_argv(args); + int result = cmd_echo(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_echo_multiple_words) { + std::vector args = {"echo", "hello", "world"}; + auto argv = make_argv(args); + int result = cmd_echo(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_echo_empty_args) { + std::vector args = {"echo"}; + auto argv = make_argv(args); + int result = cmd_echo(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 2); // Should fail with usage error +} + +TEST(test_echo_special_characters) { + std::vector args = {"echo", "hello!@#$%"}; + auto argv = make_argv(args); + int result = cmd_echo(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_echo_many_words) { + std::vector args = {"echo", "one", "two", "three", "four", "five"}; + auto argv = make_argv(args); + int result = cmd_echo(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +// ==================== Info Tests ==================== + +TEST(test_info_returns_success) { + std::vector args = {"info"}; + auto argv = make_argv(args); + int result = cmd_info(static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +// ==================== Dispatch Tests ==================== + +TEST(test_dispatch_hello) { + std::vector args = {"hello"}; + auto argv = make_argv(args); + int result = dispatch("hello", static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_dispatch_echo) { + std::vector args = {"echo", "test"}; + auto argv = make_argv(args); + int result = dispatch("echo", static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_dispatch_sum) { + std::vector args = {"sum", "1", "2"}; + auto argv = make_argv(args); + int result = dispatch("sum", static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_dispatch_info) { + std::vector args = {"info"}; + auto argv = make_argv(args); + int result = dispatch("info", static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_dispatch_help_short) { + std::vector args = {"-h"}; + auto argv = make_argv(args); + int result = dispatch("-h", static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_dispatch_help_long) { + std::vector args = {"--help"}; + auto argv = make_argv(args); + int result = dispatch("--help", static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_dispatch_help_word) { + std::vector args = {"help"}; + auto argv = make_argv(args); + int result = dispatch("help", static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 0); +} + +TEST(test_dispatch_unknown_command) { + std::vector args = {"unknown"}; + auto argv = make_argv(args); + int result = dispatch("unknown", static_cast(argv.size()), argv.data()); + ASSERT_EQ(result, 2); +} + +// ==================== Print Help Tests ==================== + +TEST(test_print_help_does_not_throw) { + // Just ensure it doesn't throw + print_help(); +} + +// ==================== Main ==================== + +int main() { + std::cout << "Running C++ unit tests...\n" << std::endl; + + // Hello tests + RUN_TEST(test_hello_default_name); + RUN_TEST(test_hello_custom_name); + RUN_TEST(test_hello_name_with_spaces); + RUN_TEST(test_hello_empty_name); + + // Sum tests + RUN_TEST(test_sum_positive_numbers); + RUN_TEST(test_sum_negative_numbers); + RUN_TEST(test_sum_both_negative); + RUN_TEST(test_sum_zero); + RUN_TEST(test_sum_large_numbers); + RUN_TEST(test_sum_wrong_arg_count_one); + RUN_TEST(test_sum_wrong_arg_count_none); + RUN_TEST(test_sum_invalid_first_integer); + RUN_TEST(test_sum_invalid_second_integer); + RUN_TEST(test_sum_float_rejected); + + // Echo tests + RUN_TEST(test_echo_single_word); + RUN_TEST(test_echo_multiple_words); + RUN_TEST(test_echo_empty_args); + RUN_TEST(test_echo_special_characters); + RUN_TEST(test_echo_many_words); + + // Info tests + RUN_TEST(test_info_returns_success); + + // Dispatch tests + RUN_TEST(test_dispatch_hello); + RUN_TEST(test_dispatch_echo); + RUN_TEST(test_dispatch_sum); + RUN_TEST(test_dispatch_info); + RUN_TEST(test_dispatch_help_short); + RUN_TEST(test_dispatch_help_long); + RUN_TEST(test_dispatch_help_word); + RUN_TEST(test_dispatch_unknown_command); + + // Print help test + RUN_TEST(test_print_help_does_not_throw); + + std::cout << "\n========================================" << std::endl; + std::cout << "Tests: " << tests_passed << "/" << tests_run << " passed" << std::endl; + + return tests_passed == tests_run ? 0 : 1; +} From b57ef1e3f3a70a37157b8a6c36f8d2cbd3221cad Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:38 +0000 Subject: [PATCH 14/18] build: update C++ CMake configuration for sdk-template --- native/cpp/CMakeLists.txt | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/native/cpp/CMakeLists.txt b/native/cpp/CMakeLists.txt index 7523d7e..5105bd9 100644 --- a/native/cpp/CMakeLists.txt +++ b/native/cpp/CMakeLists.txt @@ -5,17 +5,37 @@ project(cli_template_native_cpp VERSION 0.1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -add_executable(cli_template_native_cpp src/main.cpp) -set_target_properties(cli_template_native_cpp PROPERTIES OUTPUT_NAME "cli-template-native-cpp") +# Coverage option +option(COVERAGE "Enable code coverage" OFF) -target_sources( - cli_template_native_cpp - PRIVATE - src/echo.cpp - src/hello.cpp - src/info.cpp - src/sum.cpp +# Source files (shared between main executable and tests) +set(LIB_SOURCES + src/dispatch.cpp + src/echo.cpp + src/hello.cpp + src/info.cpp + src/sum.cpp ) +# Coverage flags +if(COVERAGE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -fprofile-arcs -ftest-coverage") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") +endif() + +# Main executable +add_executable(cli_template_native_cpp src/main.cpp ${LIB_SOURCES}) +set_target_properties(cli_template_native_cpp PROPERTIES OUTPUT_NAME "cli-template-native-cpp") target_compile_definitions(cli_template_native_cpp PRIVATE CLI_TEMPLATE_VERSION="${PROJECT_VERSION}") +target_include_directories(cli_template_native_cpp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + +# Test executable +option(BUILD_TESTS "Build unit tests" ON) +if(BUILD_TESTS) + enable_testing() + add_executable(cli_template_native_cpp_tests tests/test_functions.cpp ${LIB_SOURCES}) + target_compile_definitions(cli_template_native_cpp_tests PRIVATE CLI_TEMPLATE_VERSION="${PROJECT_VERSION}") + target_include_directories(cli_template_native_cpp_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + add_test(NAME cpp_unit_tests COMMAND cli_template_native_cpp_tests) +endif() From 31303aa8875704a1d6c2e19ccf6c1e1031317938 Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:43 +0000 Subject: [PATCH 15/18] chore: update clang-tidy configuration --- .clang-tidy | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 02a59b4..5765012 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,12 +1,20 @@ +# Minimal clang-tidy config for development - focuses on real bugs Checks: > -*, bugprone-*, - performance-*, - portability-*, - readability-*, - modernize-*, - cppcoreguidelines-*, - -cppcoreguidelines-avoid-magic-numbers + clang-analyzer-*, + misc-unused-*, + modernize-use-nullptr, + modernize-use-override, + performance-for-range-copy, + performance-implicit-conversion-in-loop, + performance-inefficient-algorithm, + performance-move-const-arg, + performance-unnecessary-copy-initialization, + readability-misleading-indentation, + readability-redundant-*, + -bugprone-easily-swappable-parameters + WarningsAsErrors: "" HeaderFilterRegex: "native/cpp/.*" FormatStyle: file From d933e889338d8387b9ff9c6bc44f97542436f103 Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:48 +0000 Subject: [PATCH 16/18] chore: update gitignore for build artifacts and coverage --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 419bb7b..8001c4f 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +coverage.info *.cover *.py.cover .hypothesis/ From ce2f2a803ced752ca8458ce3839557f0be81667b Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:52 +0000 Subject: [PATCH 17/18] docs: update README with sdk-template information --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 72efd3d..5ea63cd 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,76 @@ -# cli-template +# sdk-template -Production-ready starting point for building a CLI, with: +![CI](https://img.shields.io/badge/ci-github%20actions-2088FF?logo=githubactions&logoColor=white) +![Dev Container](https://img.shields.io/badge/devcontainer-ready-2496ED?logo=docker&logoColor=white) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-3776AB?logo=python&logoColor=white)](https://www.python.org/downloads/) +[![Rust 2021](https://img.shields.io/badge/rust-2021-orange?logo=rust&logoColor=white)](https://www.rust-lang.org/) +[![C++20](https://img.shields.io/badge/C%2B%2B-20-00599C?logo=c%2B%2B&logoColor=white)](https://isocpp.org/) +[![Lint: Ruff](https://img.shields.io/badge/lint-ruff-25c2a0?logo=python&logoColor=white)](https://docs.astral.sh/ruff/) +![Types: Pyright](https://img.shields.io/badge/types-pyright-3776AB?logo=python&logoColor=white) +![Tests: pytest](https://img.shields.io/badge/tests-pytest-0A9EDC?logo=pytest&logoColor=white) +![Rust Lints: clippy](https://img.shields.io/badge/rust%20lints-clippy-b7410e?logo=rust&logoColor=white) +![Build: CMake+Ninja](https://img.shields.io/badge/build-cmake%2Bninja-064F8C?logo=cmake&logoColor=white) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://pre-commit.com/) +[![Commitizen](https://img.shields.io/badge/commitizen-friendly-orange)](https://commitizen-tools.github.io/commitizen/) +[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE) + +Multilingual SDK/CLI template with Rust/C++ backends for performance-bound tasks. Production-ready starting point for building a CLI, with: - Dev Container + VS Code defaults for Python (`ruff`), Rust (`rust-analyzer`, `clippy`) and C++ (`clang-format`, `cppcheck`) - `pre-commit` hooks for formatting/linting - `commitizen` for Conventional Commits + changelog/version bumping -## Quickstart +## Prerequisites + +- [VS Code](https://code.visualstudio.com/) with [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) +- [Docker](https://www.docker.com/get-started) + +## Getting Started + +### 1. Create your project + +```bash +# Clone and rename +git clone https://github.com/YOUR_USERNAME/cli-template.git my-cli +cd my-cli +rm -rf .git && git init -b main +``` + +### 2. Customize for your CLI + +Replace `cli-template` / `cli_template` with your project name: + +| File | What to change | +| --------------------------------- | ------------------------------------------ | +| `pyproject.toml` | `name`, `description`, `[project.scripts]` | +| `src/cli_template/` | Rename folder to your package name | +| `.devcontainer/devcontainer.json` | `"name"` field | +| `native/rust/Cargo.toml` | `name`, `version` | +| `native/cpp/CMakeLists.txt` | `project()` name | + +### 3. Open in Dev Container + +1. Open folder in VS Code +2. When prompted, click **Reopen in Container** (or run `Dev Containers: Reopen in Container`) +3. Wait for container to build (~2-3 min first time) + +### 4. Set up git hooks + +```bash +pre-commit install +pre-commit install --hook-type commit-msg +``` -1. Open in VS Code -2. Run **Dev Containers: Reopen in Container** -3. Install git hooks: +## Project Structure - - `pre-commit install` - - `pre-commit install --hook-type commit-msg` +``` +src/cli_template/ # Python CLI (main entry point) +native/rust/ # Optional Rust backend +native/cpp/ # Optional C++ backend +tests/ # Test files +scripts/ # Build/release scripts +docs/ # Documentation +``` ## Run the CLI From e72d18996a5c465ad9bca79ac02ac90fea030c12 Mon Sep 17 00:00:00 2001 From: Daniel Phan Date: Sat, 20 Dec 2025 20:45:56 +0000 Subject: [PATCH 18/18] chore: remove obsolete scripts directory --- scripts/README.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 scripts/README.md diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 8dbf4b2..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Scripts - -Helper scripts for development and release automation. - -Recommended: keep scripts idempotent and runnable from repo root. -