From 199d322f8623de27ae9e44371e4f365846ed1605 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 1 Aug 2025 15:50:50 +0200 Subject: [PATCH 001/199] Complete initial set-up for secret management through 1Password CLI --- .envrc | 11 +++ .github/workflows/ci.yml | 36 +++++++ .gitignore | 96 ++++++++++++++++++ .pre-commit-config.yaml | 20 ++++ .secrets.baseline | 127 ++++++++++++++++++++++++ docs/secret_management.md | 148 ++++++++++++++++++++++++++++ example/__init__.py | 0 example/using_secrets.ipynb | 80 +++++++++++++++ example/using_secrets.py | 0 requirements.in | 3 + requirements.txt | 70 +++++++++++++ scripts/dependency.sh | 37 +++++++ scripts/hooks/check-requirements.sh | 8 ++ scripts/setup.sh | 119 ++++++++++++++++++++++ utils/__init__.py | 0 utils/secrets.py | 29 ++++++ utils/utils.py | 10 ++ 17 files changed, 794 insertions(+) create mode 100644 .envrc create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .secrets.baseline create mode 100644 docs/secret_management.md create mode 100644 example/__init__.py create mode 100644 example/using_secrets.ipynb create mode 100644 example/using_secrets.py create mode 100644 requirements.in create mode 100644 requirements.txt create mode 100755 scripts/dependency.sh create mode 100755 scripts/hooks/check-requirements.sh create mode 100755 scripts/setup.sh create mode 100644 utils/__init__.py create mode 100644 utils/secrets.py create mode 100644 utils/utils.py diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..27996c2f --- /dev/null +++ b/.envrc @@ -0,0 +1,11 @@ +# Load environment variables from .env if it exists +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +fi + +# Set a virtualenv automatically if venv exists +if [ -d venv ]; then + source venv/bin/activate +fi + +export PYTHONPATH=. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..103d6f7f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +# .github/workflows/ci.yml + +name: CI + +on: + push: + branches: + - main + - staging + pull_request: + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + + env: + API_KEY: ${{ secrets.API_KEY }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + pip install -r requirements.txt + + - name: Run tests + run: | + pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..01661b5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,96 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ + +# Jupyter Notebook checkpoints +.ipynb_checkpoints/ + +# VSCode settings +.vscode/ + +# PyCharm settings +.idea/ + +# macOS files +.DS_Store + +# mypy type checker cache +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype +.pytype/ + +# Pytest cache +.pytest_cache/ + +# Coverage reports +htmlcov/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml + +# Test output +test-results/ + +# Build / distribution +build/ +dist/ +*.egg-info/ +.eggs/ +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage tools +.tox/ +.nox/ + +# Python wheels +*.whl + +# Editable installs +*.egg-link + +# dotenv files (if used for secrets) +.env +.env.* + +# Poetry +poetry.lock + +# PDM +__pypackages__/ + +# PyInstaller +*.spec + +# Log files +*.log + +# Temporary files +*.bak +*.swp +*.swo +*~ + +# Local notebooks +notebooks-local/ + +# direnv +.direnv/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..fbb96177 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: local + hooks: + - id: prevent-manual-requirements-edits + name: Prevent manual changes to requirements.txt + entry: scripts/hooks/check-requirements.sh + language: script + types: [text] + + + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ['--baseline', '.secrets.baseline'] + + - repo: https://github.com/kynan/nbstripout + rev: 0.8.1 + hooks: + - id: nbstripout diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 00000000..f2bf497f --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,127 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": {}, + "generated_at": "2025-08-01T13:26:53Z" +} diff --git a/docs/secret_management.md b/docs/secret_management.md new file mode 100644 index 00000000..d4ede5fe --- /dev/null +++ b/docs/secret_management.md @@ -0,0 +1,148 @@ +# Secret management + +We use **1Password CLI** (`op`) and **GitHub Secrets** to fetch secrets securelyโ€”**no plaintext `.env` files** are ever stored in Git. + +Perform all of the following in your development environment on WSL. + +### 1.2.1 Prerequisites (from scratch) + +1. Verify 1Password cli is set up correctly + + ```bash + op --version + ``` + + This should show the version number. If not check the steps from 1.1.1 + +2. **Create virtual environment** + + ```bash + python3 -m venv venv + source venv/bin/activate + ``` + +3. **Install Dev Dependencies** + + ```bash + pip install -r requirements-dev.txt + ``` + +4. **Install Direnv** and allow + + ```bash + sudo apt install direnv + direnv allow + ``` + +5. Then add this to your shell profile (`~/.bashrc` ) + + ```bash + eval "$(direnv hook bash)" + ``` + +6. Then reload your shell: + + ```bash + source ~/.bashrc + ``` + + +--- + +### 1.2.2 Local Development + +1. **Authentication for 1Password is handled automatically** + + Once per session you will be asked to authenticate through the 1Password Interface. **Never** log in manually through the cli. This will overwrite the connection. + +2. **Enable Direnv when changing .envrc** +Whenever you make any changes to `.envrc` always allow Direnv to load environment variables automatically: + + ```bash + direnv allow + ``` + +3. **Environment Variables** +We load secrets on demand from 1Passwordโ€”no `.env` file. This can be done dynamically using the provided utility function `get_secret` in `utils/secrets.py` . Use it as follows: + + ```python + import os + from utils.secrets import get_secret + + # To find the path to your secret go to 1Password Interace > your item > click dropdown next to field > "Copy secret reference" + + # Load once using + API_KEY = get_secret("op://Shared with all/RAGFLow API Key/credential") + + # Or load and save to use accross the environment: + get_secret("op://Shared with all/RAGFLow API Key/credential", "API_KEY") + API_KEY = os.getenv('API_KEY') + ``` + + + +4. **Pre-commit Hooks** +On each commit, secrets are blocked and notebook outputs cleared: + - `detect-secrets` + - `nbstripout` + + These hooks are configured in `.pre-commit-config.yaml` and installed via: + + ```bash + pre-commit install + pre-commit autoupdate + ``` + + +--- + +### 1.2.3 CI / GitHub Actions + +For CI, we **do not** use 1Password directly. Instead, define your secrets in **GitHub Settings > Secrets**: + +- `API_KEY` +- `DB_PASSWORD` + +Then in your workflow (`.github/workflows/ci.yml`): + +```yaml +name: CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run tests + env: + API_KEY: ${{ secrets.API_KEY }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + run: pytest + +``` + +This way, CI runners receive secrets securely from GitHub and never see 1Password directly. + +--- + +### 1.2.4 Rotating & Auditing + +- **Rotate** immediately in 1Password or GitHub when a secret is compromised. +- **Audit** with `detect-secrets scan > .secrets.baseline` after updates. +- Maintain a clear **playbook** in `SECURITY.md` for incident response. \ No newline at end of file diff --git a/example/__init__.py b/example/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/example/using_secrets.ipynb b/example/using_secrets.ipynb new file mode 100644 index 00000000..7b3f0e11 --- /dev/null +++ b/example/using_secrets.ipynb @@ -0,0 +1,80 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "### Add Project root to system path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from pathlib import Path\n", + "sys.path.append(str(next(p for p in [Path.cwd()] + list(Path.cwd().parents) if (p / \".git\").exists())))" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "### Get secrets dynamically from 1Password vaults" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from utils.secrets import get_secret\n", + "\n", + "# Use the get_secret function to retrieve your secret. \n", + "# To find the path to your secret go to 1Password Interace > your item > click dropdown next to field > \"Copy secret reference\"\n", + "api_key = get_secret(\"op://Shared with all/RAGFLow API Key/credential\")\n", + "if api_key:\n", + " print(f\"API_KEY is set: {api_key}\")\n", + "else:\n", + " print(\"API_KEY is not set.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv (3.12.3)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/example/using_secrets.py b/example/using_secrets.py new file mode 100644 index 00000000..e69de29b diff --git a/requirements.in b/requirements.in new file mode 100644 index 00000000..e9c90b8e --- /dev/null +++ b/requirements.in @@ -0,0 +1,3 @@ +pre-commit +detect-secrets +nbstripout \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c8a88037 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,70 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile requirements.in +# +attrs==25.3.0 + # via + # jsonschema + # referencing +certifi==2025.7.14 + # via requests +cfgv==3.4.0 + # via pre-commit +charset-normalizer==3.4.2 + # via requests +detect-secrets==1.5.0 + # via -r requirements.in +distlib==0.4.0 + # via virtualenv +fastjsonschema==2.21.1 + # via nbformat +filelock==3.18.0 + # via virtualenv +identify==2.6.12 + # via pre-commit +idna==3.10 + # via requests +jsonschema==4.25.0 + # via nbformat +jsonschema-specifications==2025.4.1 + # via jsonschema +jupyter-core==5.8.1 + # via nbformat +nbformat==5.10.4 + # via nbstripout +nbstripout==0.8.1 + # via -r requirements.in +nodeenv==1.9.1 + # via pre-commit +platformdirs==4.3.8 + # via + # jupyter-core + # virtualenv +pre-commit==4.2.0 + # via -r requirements.in +pyyaml==6.0.2 + # via + # detect-secrets + # pre-commit +referencing==0.36.2 + # via + # jsonschema + # jsonschema-specifications +requests==2.32.4 + # via detect-secrets +rpds-py==0.26.0 + # via + # jsonschema + # referencing +traitlets==5.14.3 + # via + # jupyter-core + # nbformat +typing-extensions==4.14.1 + # via referencing +urllib3==2.5.0 + # via requests +virtualenv==20.32.0 + # via pre-commit diff --git a/scripts/dependency.sh b/scripts/dependency.sh new file mode 100755 index 00000000..5adf1f61 --- /dev/null +++ b/scripts/dependency.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -e + +# Config +VENV_DIR="venv" + +# Colors +YELLOW='\033[1;33m' +GREEN='\033[1;32m' +RESET='\033[0m' + +echo -e "${YELLOW}๐Ÿ“ฆ Activating virtualenv...${RESET}" +if [ -f "$VENV_DIR/bin/activate" ]; then + source "$VENV_DIR/bin/activate" +else + echo "โŒ No virtualenv found at $VENV_DIR. Run: python -m venv $VENV_DIR" + exit 1 +fi + +echo -e "${YELLOW}๐Ÿ” Checking pip-tools...${RESET}" +if ! pip show pip-tools &>/dev/null; then + echo -e "${YELLOW}โžก๏ธ Installing pip-tools...${RESET}" + pip install pip-tools +else + echo -e "${GREEN}โœ… pip-tools is already installed.${RESET}" +fi + +echo -e "${YELLOW}๐Ÿงฎ Compiling requirements.txt from requirements.in...${RESET}" +pip-compile requirements.in + +echo -e "${YELLOW}๐Ÿ“ฅ Installing compiled dependencies...${RESET}" +pip install -r requirements.txt + +echo -e "${YELLOW}๐Ÿ” Verifying environment with pip check...${RESET}" +pip check + +echo -e "${GREEN}โœ… Setup complete! All dependencies are installed and compatible.${RESET}" diff --git a/scripts/hooks/check-requirements.sh b/scripts/hooks/check-requirements.sh new file mode 100755 index 00000000..44a144d2 --- /dev/null +++ b/scripts/hooks/check-requirements.sh @@ -0,0 +1,8 @@ +#!/bin/bash +if git diff --cached --name-only | grep -q "^requirements.txt$"; then + if ! grep -q "# This file is autogenerated by pip-compile" requirements.txt; then + echo "โŒ ERROR: requirements.txt appears to have been modified manually." + echo "Use 'pip-compile' instead. See 2.2 in the docs." + exit 1 + fi +fi \ No newline at end of file diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 00000000..951acb50 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,119 @@ +#!/bin/bash +set -e + +# Colors +GREEN='\033[0;32m' +CYAN='\033[1;36m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +RESET='\033[0m' + +step() { + echo -e "${CYAN}๐Ÿ‘‰ $1${RESET}" +} + +success() { + echo -e "${GREEN}โœ… $1${RESET}" +} + +warn() { + echo -e "${YELLOW}โš ๏ธ $1${RESET}" +} + +error() { + echo -e "${RED}โŒ $1${RESET}" + exit 1 +} + +# ------------------------------------------------------ +# ๐Ÿ” SECTION: Secret Management Setup +# ------------------------------------------------------ + +setup_secret_management() { + step "Checking system requirements..." + + command -v python3 >/dev/null || error "Python 3 is not installed." + command -v pip >/dev/null || error "pip is not installed." + command -v curl >/dev/null || error "curl is not installed." + + # --- 1Password CLI check and setup --- + if op --version &>/dev/null; then + success "1Password CLI is installed." + else + warn "1Password CLI is installed, but not yet connected to your account in WSL." + + echo -e "${YELLOW}๐Ÿ‘‰ 1Password must be installed on Windows with CLI integration enabled, and linked to WSL.${RESET}" + echo -e "${YELLOW}Follow the instructions here:${RESET}" + echo -e "${CYAN}https://www.notion.so/techletes/Python-Basic-Template-Repository-24117d03144d809f92f9dc682d17d347${RESET}\n" + echo -e "${YELLOW}Re-run setup script when done${RESET}" + fi + + # Virtual environment + step "Creating Python virtual environment..." + python3 -m venv venv + source venv/bin/activate + success "Virtual environment activated." + + + + # Dev dependencies + step "Installing development dependencies using pip-tools" + pip install pip-tools + succesfull "pip-tools installed." + + echo "Compiling requirements using pip-tools..." + + if [[ -f "requirements.in" ]]; then + pip-compile requirements.in --output-file requirements.txt + success "Dev dependencies installed." + else + warn "requirements-dev.txt not found. Skipping." + fi + + # Install direnv + step "Installing direnv..." + sudo apt install -y direnv + success "Direnv installed." + + # Add direnv hook to shell profile + PROFILE="${HOME}/.bashrc" + if ! grep -q 'direnv hook bash' "$PROFILE"; then + echo 'eval "$(direnv hook bash)"' >> "$PROFILE" + success "Added direnv hook to $PROFILE." + else + warn "Direnv hook already present in $PROFILE." + fi + + # Reload shell config + step "Reloading your shell config..." + source "$PROFILE" + + # Direnv allow in project root + step "Enabling direnv for this project..." + direnv allow || warn "Make sure .envrc exists and is correct." + + # Install pre-commit hooks + step "Installing pre-commit hooks..." + if [[ -f ".pre-commit-config.yaml" ]]; then + source venv/bin/activate + pre-commit install + pre-commit autoupdate + success "Pre-commit hooks installed and updated." + else + warn ".pre-commit-config.yaml not found. Skipping hook installation." + fi + + # Set baseline secrets + step "Setting baseline secrets with detect-secrets..." + detect-secrets scan > .secrets.baseline + success "Baseline secrets set in .secrets.baseline." + + echo -e "\n${GREEN}๐Ÿš€ Secret management setup complete. Happy coding!${RESET}" + echo -e "\n${GREEN}๐Ÿš€ To test if the 1Password integration was succesfull try running 'example/using_secrets.py'${RESET}" +} + +# ------------------------------------------------------ +# ๐Ÿ Main Execution +# ------------------------------------------------------ + +setup_secret_management \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/utils/secrets.py b/utils/secrets.py new file mode 100644 index 00000000..a7eb7b6c --- /dev/null +++ b/utils/secrets.py @@ -0,0 +1,29 @@ +# A tiny helper module to load secrets at runtime if you canโ€™t rely on direnv +# This uses the 1Password CLI to fetch secrets on-the-fly in any script or app + +import subprocess, re +import os + +def get_secret(path: str, env_var: str = None) -> str: + """ + Fetch a secret from 1Password CLI and optionally set it as an environment variable. + + Args: + path (str): The secret path in the format 'op://Vault name/Item name/field'. + env_var (str, optional): If provided, the fetched secret will be set to this environment variable. + + Returns: + str: The secret value fetched from 1Password. + + Raises: + ValueError: If the path format is invalid. + subprocess.CalledProcessError: If the 'op' CLI command fails. + """ + # Validate path format: op://Vault name/Item name/field + pattern = r"^op://[^/]+/[^/]+/[^/]+$" + if not re.match(pattern, path): + raise ValueError("Secret path must be of the form 'op://Vault name/Item name/field'") + secret = subprocess.check_output(['op', 'read', path]).decode().strip() + if env_var: + os.environ[env_var] = secret + return secret diff --git a/utils/utils.py b/utils/utils.py new file mode 100644 index 00000000..29392ba7 --- /dev/null +++ b/utils/utils.py @@ -0,0 +1,10 @@ +import sys +from pathlib import Path + +def add_project_root(marker: str = ".git") -> Path: + path = Path(__file__).resolve() + for parent in path.parents: + if (parent / marker).exists(): + sys.path.append(str(parent)) + return + raise RuntimeError(f"Project root not found (no '{marker}' in any parent directories)") From 3a411e03fb7e1fe8cefad57bc6af2a5cfcc19820 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 1 Aug 2025 15:52:52 +0200 Subject: [PATCH 002/199] Update secret management setup and streamline development dependencies installation --- .secrets.baseline | 2 +- scripts/setup.sh | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index f2bf497f..d1b8b2e3 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -123,5 +123,5 @@ } ], "results": {}, - "generated_at": "2025-08-01T13:26:53Z" + "generated_at": "2025-08-01T13:52:27Z" } diff --git a/scripts/setup.sh b/scripts/setup.sh index 951acb50..096da34e 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -57,18 +57,8 @@ setup_secret_management() { # Dev dependencies - step "Installing development dependencies using pip-tools" - pip install pip-tools - succesfull "pip-tools installed." - - echo "Compiling requirements using pip-tools..." - - if [[ -f "requirements.in" ]]; then - pip-compile requirements.in --output-file requirements.txt - success "Dev dependencies installed." - else - warn "requirements-dev.txt not found. Skipping." - fi + step "Setting up development dependencies" + ./scripts/dependency.sh # Install direnv step "Installing direnv..." From 14bd4878075383503c2e3d1706639812630b21ab Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 1 Aug 2025 15:56:49 +0200 Subject: [PATCH 003/199] Implement secret retrieval using 1Password CLI in using_secrets.py --- example/using_secrets.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/example/using_secrets.py b/example/using_secrets.py index e69de29b..45a36736 100644 --- a/example/using_secrets.py +++ b/example/using_secrets.py @@ -0,0 +1,10 @@ + +from utils.secrets import get_secret + +# Use the get_secret function to retrieve your secret. +# To find the path to your secret go to 1Password Interace > your item > click dropdown next to field > "Copy secret reference" +api_key = get_secret("op://Shared with all/RAGFLow API Key/credential") +if api_key: + print(f"API_KEY is set: {api_key}") +else: + print("API_KEY is not set.") \ No newline at end of file From a73bfd19f0958535096b86ef64a51802e97dea0e Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 1 Aug 2025 15:58:33 +0200 Subject: [PATCH 004/199] Add jupytext hook for Jupyter notebook format management --- .pre-commit-config.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fbb96177..a55429a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,3 +18,14 @@ repos: rev: 0.8.1 hooks: - id: nbstripout + args: ["--diff"] + + - repo: https://github.com/mwouts/jupytext + rev: v1.14.1 + hooks: + - id: jupytext + name: jupytext + entry: jupytext --pre-commit-mode --set-formats "ipynb,py:percent" + require_serial: true + types_or: [jupyter] + From daed6d5f2a9d4374ad28164f2bf2d7f13b1d6c2a Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 1 Aug 2025 16:07:01 +0200 Subject: [PATCH 005/199] Refactor pre-commit configuration and add nbstripout auto-add hook for Jupyter notebooks --- .pre-commit-config.yaml | 30 +++++++++++++---------------- scripts/hooks/nbstripout-autoadd.sh | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 17 deletions(-) create mode 100755 scripts/hooks/nbstripout-autoadd.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a55429a8..e6714406 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,31 +1,27 @@ repos: - repo: local hooks: - - id: prevent-manual-requirements-edits - name: Prevent manual changes to requirements.txt - entry: scripts/hooks/check-requirements.sh - language: script - types: [text] + - id: prevent-manual-requirements-edits + name: Prevent manual changes to requirements.txt + entry: scripts/hooks/check-requirements.sh + language: script + types: [text] + + - id: nbstripout-autoadd + name: nbstripout (auto-add notebooks) + entry: scripts/hooks/nbstripout-autoadd.sh + language: script + types: [jupyter] - - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 hooks: - id: detect-secrets args: ['--baseline', '.secrets.baseline'] - - repo: https://github.com/kynan/nbstripout - rev: 0.8.1 - hooks: - - id: nbstripout - args: ["--diff"] - - repo: https://github.com/mwouts/jupytext - rev: v1.14.1 + rev: v1.14.7 hooks: - id: jupytext - name: jupytext - entry: jupytext --pre-commit-mode --set-formats "ipynb,py:percent" + args: [--sync] require_serial: true - types_or: [jupyter] - diff --git a/scripts/hooks/nbstripout-autoadd.sh b/scripts/hooks/nbstripout-autoadd.sh new file mode 100755 index 00000000..8220c4ab --- /dev/null +++ b/scripts/hooks/nbstripout-autoadd.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +# Run nbstripout on all files passed by pre-commit +nbstripout "$@" + +# Re-add any modified .ipynb files to the index +modified=$(git ls-files -m '*.ipynb') || true + +if [ -n "$modified" ]; then + echo "๐Ÿ” Re-staging modified notebooks:" + echo "$modified" + git add $modified +fi From 5fdf2abf43f8b1fd5f3b9acb6200e9bdf3c90f16 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 1 Aug 2025 16:09:47 +0200 Subject: [PATCH 006/199] Change nbstripout hook language to Python and add additional dependencies --- .pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6714406..9d1c1695 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,8 +10,9 @@ repos: - id: nbstripout-autoadd name: nbstripout (auto-add notebooks) entry: scripts/hooks/nbstripout-autoadd.sh - language: script + language: python types: [jupyter] + additional_dependencies: [nbstripout] - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 From b9f86d4db73ae7a22c617de9a59b6225520eada6 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 1 Aug 2025 16:24:23 +0200 Subject: [PATCH 007/199] Add CI workflow configuration and enhance secret management setup --- .github/workflows/{ci.yml => ci.yml.example} | 0 scripts/setup.sh | 7 +++++-- 2 files changed, 5 insertions(+), 2 deletions(-) rename .github/workflows/{ci.yml => ci.yml.example} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml.example similarity index 100% rename from .github/workflows/ci.yml rename to .github/workflows/ci.yml.example diff --git a/scripts/setup.sh b/scripts/setup.sh index 096da34e..1f9b54f8 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -54,8 +54,6 @@ setup_secret_management() { source venv/bin/activate success "Virtual environment activated." - - # Dev dependencies step "Setting up development dependencies" ./scripts/dependency.sh @@ -93,6 +91,11 @@ setup_secret_management() { warn ".pre-commit-config.yaml not found. Skipping hook installation." fi + sudo apt update + sudo apt install locales + sudo locale-gen en_US.UTF-8 + sudo update-locale LANG=en_US.UTF-8 + # Set baseline secrets step "Setting baseline secrets with detect-secrets..." detect-secrets scan > .secrets.baseline From d7e277f302fb84b2f293162711a84f1c72fd3b51 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 4 Aug 2025 14:54:49 +0200 Subject: [PATCH 008/199] Update dependency management setup through pyproject.toml --- .pre-commit-config.yaml | 19 +++ docs/dependency_management.md | 148 ++++++++++++++++++++ pyproject.toml | 139 ++++++++++++++++++ requirements.in | 5 +- requirements.txt | 64 --------- scripts/dependency.sh | 12 +- scripts/setup.sh | 255 ++++++++++++++++++++++++++++------ 7 files changed, 529 insertions(+), 113 deletions(-) create mode 100644 docs/dependency_management.md create mode 100644 pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9d1c1695..5073ba9c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,14 @@ repos: - repo: local hooks: + # Prevent manual requirements.txt files - id: prevent-manual-requirements-edits name: Prevent manual changes to requirements.txt entry: scripts/hooks/check-requirements.sh language: script types: [text] + # Strip notebook outputs - id: nbstripout-autoadd name: nbstripout (auto-add notebooks) entry: scripts/hooks/nbstripout-autoadd.sh @@ -14,15 +16,32 @@ repos: types: [jupyter] additional_dependencies: [nbstripout] + # Secrets scanning - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 hooks: - id: detect-secrets args: ['--baseline', '.secrets.baseline'] + # Notebook/text sync - repo: https://github.com/mwouts/jupytext rev: v1.14.7 hooks: - id: jupytext args: [--sync] require_serial: true + + # Code formatting: Black + - repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black + language_version: python3 + + # Import sorting: isort (Black compatible) + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: ["--profile=black"] + language_version: python3 diff --git a/docs/dependency_management.md b/docs/dependency_management.md new file mode 100644 index 00000000..a7b33062 --- /dev/null +++ b/docs/dependency_management.md @@ -0,0 +1,148 @@ +# 2. Dependency management + +This project uses a modern Python dependency management approach combining [`pip-tools`](https://etc.com/jazzband/pip-tools) for production dependencies and `pyproject.toml` for development tools. + +Dependencies are separated into two categories: +- **Production dependencies**: Managed via `requirements.in` โ†’ `requirements.txt` +- **Development dependencies**: Managed via `pyproject.toml` + +--- + +## 2.1 Overview of the dependency workflow + +The project separates different types of dependencies: + +- **`requirements.in`**: Production dependencies that your project needs to run (e.g. `requests`, `pandas`, `fastapi`). +- **`requirements.txt`**: Generated by `pip-tools`, contains all resolved production packages with pinned versions. +- **`pyproject.toml`**: Development tools and testing dependencies (e.g. `pre-commit`, `pytest`, `black`, `mypy`). + +This approach provides: +- Clean separation between production and development dependencies +- Reproducible builds with pinned versions +- Optional installation of development tools +- Consistency across environments + +--- + +## 2.2 Installation Workflow + +To add dependencies, follow these steps based on the type: + +### For Production Dependencies + +1. **Add the dependency to `requirements.in`** + + For example, to add `requests`: + + ``` + # Core project dependencies go here + # Development dependencies are in pyproject.toml [project.optional-dependencies.dev] + requests + pandas + ``` + + + +2. **Run the dependency script** + + Use the project's dependency script to resolve and install: + + ```bash + ./scripts/dependency.sh + ``` + + **This will:** + + - Activate your virtual environment (from `.venv/`) + - Install `pip-tools` if needed + - Compile `requirements.txt` from `requirements.in` + - Install all dependencies + - Run `pip check` to ensure compatibility + +3. **Commit the changes** + + Always commit both files: + + ```bash + git add requirements.in requirements.txt + git commit -m "Add requests dependency" + ``` + +### For Development Dependencies + +To add development tools (linters, formatters, testing tools): + +1. **Add to `pyproject.toml`** + + Edit the `[project.optional-dependencies.dev]` section: + + ```toml + [project.optional-dependencies] + dev = [ + "pre-commit", + "detect-secrets", + "nbstripout", + "pytest", + "pytest-cov", + "black", + "isort", + "flake8", + "mypy", + "pip-tools", + "your-new-tool", # Add here + ] + ``` + +2. **Install the updated dependencies** + + ```bash + pip install -e .[dev] + ``` + +### Installation Options + +- **Minimal (production only)**: `pip install -e .` +- **Full development setup**: `pip install -e .[dev]` +- **Using the script**: `./scripts/dependency.sh` (installs both) +--- + +## 2.4 Sanity Check + +To manually verify that all installed dependencies are compatible, run: + +```bash +pip check +``` + +This is automatically done by the setup script as well. + +--- + +## 2.5 Summary of Rules + +| โœ… Do This | โŒ Don't Do This | +| --- | --- | +| Add production deps to `requirements.in` | Don't run `pip install ` manually | +| Add dev tools to `pyproject.toml` | Don't edit `requirements.txt` by hand | +| Use `./scripts/dependency.sh` or `pip install -e .[dev]` | Don't mix production and dev dependencies | +| Commit both `requirements.in` and `requirements.txt` | Don't skip `pip check` or forget to compile | +| Use `pip install -e .` for minimal installs | Don't install dev tools in production | + +--- + +## 2.6 Migration from Previous Setup + +If you're updating from an older version of this template where all dependencies were in `requirements.in`: + +1. **Identify development tools** in your current `requirements.in` +2. **Move them to `pyproject.toml`** in the `[project.optional-dependencies.dev]` section +3. **Keep only production dependencies** in `requirements.in` +4. **Regenerate `requirements.txt`** with `pip-compile requirements.in` +5. **Install dev dependencies** with `pip install -e .[dev]` + +Development tools typically include: `pre-commit`, `detect-secrets`, `nbstripout`, `pytest`, `black`, `isort`, `flake8`, `mypy`, `bandit`, \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..5f113cba --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,139 @@ +# This file is only used for tool configuration. +# Dependency locking is handled via pip-tools using requirements.in / requirements.txt. + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-basic-template" +version = "0.1.0" +description = "A basic Python project template" +readme = "README.md" +requires-python = ">=3.11" +license = {text = "MIT"} +authors = [ + {name = "Your Name", email = "your.email@example.com"}, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [] + +[project.optional-dependencies] +dev = [ + "pre-commit", + "detect-secrets", + "nbstripout", + "pytest", + "pytest-cov", + "black", + "isort", + "flake8", + "mypy", + "pip-tools", # for managing requirements.in/txt +] + +[project.urls] +Homepage = "https://github.com/thom-techlete/python_basic_template" +Repository = "https://github.com/thom-techlete/python_basic_template" +Issues = "https://github.com/thom-techlete/python_basic_template/issues" + +[tool.setuptools.packages.find] +where = ["."] +include = ["utils*", "example*"] +exclude = ["tests*"] + +[tool.black] +line-length = 88 +target-version = ["py311"] + +[tool.isort] +profile = "black" +line_length = 88 +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = [ + "--strict-markers", + "--strict-config", + "--cov=utils", + "--cov=example", + "--cov-report=term-missing", + "--cov-report=html", + "--cov-report=xml", +] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] + +[tool.coverage.run] +source = ["utils", "example"] +omit = [ + "*/tests/*", + "*/venv/*", + "*/__pycache__/*", + "setup.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] + +[tool.mypy] +python_version = "3.11" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true +show_error_codes = true + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_untyped_defs = false + +[tool.flake8] +max-line-length = 88 +extend-ignore = ["E203", "W503"] +exclude = [ + ".git", + "__pycache__", + ".venv", + "venv", + "build", + "dist", + "*.egg-info", +] + +[tool.bandit] +exclude_dirs = ["tests", "venv"] +skips = ["B101", "B601"] diff --git a/requirements.in b/requirements.in index e9c90b8e..749b7f9a 100644 --- a/requirements.in +++ b/requirements.in @@ -1,3 +1,2 @@ -pre-commit -detect-secrets -nbstripout \ No newline at end of file +# Core project dependencies go here +# Development dependencies are in pyproject.toml [project.optional-dependencies.dev] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c8a88037..c71f0809 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,67 +4,3 @@ # # pip-compile requirements.in # -attrs==25.3.0 - # via - # jsonschema - # referencing -certifi==2025.7.14 - # via requests -cfgv==3.4.0 - # via pre-commit -charset-normalizer==3.4.2 - # via requests -detect-secrets==1.5.0 - # via -r requirements.in -distlib==0.4.0 - # via virtualenv -fastjsonschema==2.21.1 - # via nbformat -filelock==3.18.0 - # via virtualenv -identify==2.6.12 - # via pre-commit -idna==3.10 - # via requests -jsonschema==4.25.0 - # via nbformat -jsonschema-specifications==2025.4.1 - # via jsonschema -jupyter-core==5.8.1 - # via nbformat -nbformat==5.10.4 - # via nbstripout -nbstripout==0.8.1 - # via -r requirements.in -nodeenv==1.9.1 - # via pre-commit -platformdirs==4.3.8 - # via - # jupyter-core - # virtualenv -pre-commit==4.2.0 - # via -r requirements.in -pyyaml==6.0.2 - # via - # detect-secrets - # pre-commit -referencing==0.36.2 - # via - # jsonschema - # jsonschema-specifications -requests==2.32.4 - # via detect-secrets -rpds-py==0.26.0 - # via - # jsonschema - # referencing -traitlets==5.14.3 - # via - # jupyter-core - # nbformat -typing-extensions==4.14.1 - # via referencing -urllib3==2.5.0 - # via requests -virtualenv==20.32.0 - # via pre-commit diff --git a/scripts/dependency.sh b/scripts/dependency.sh index 5adf1f61..f9e288d4 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -e +# Dependency management script for hybrid setup: +# - Production dependencies: requirements.in -> requirements.txt (via pip-tools) +# - Development dependencies: pyproject.toml [project.optional-dependencies.dev] + # Config VENV_DIR="venv" @@ -28,10 +32,14 @@ fi echo -e "${YELLOW}๐Ÿงฎ Compiling requirements.txt from requirements.in...${RESET}" pip-compile requirements.in -echo -e "${YELLOW}๐Ÿ“ฅ Installing compiled dependencies...${RESET}" +echo -e "${YELLOW}๐Ÿ“ฅ Installing production dependencies...${RESET}" pip install -r requirements.txt -echo -e "${YELLOW}๐Ÿ” Verifying environment with pip check...${RESET}" +echo -e "${YELLOW}๏ฟฝ๏ธ Installing development dependencies...${RESET}" +pip install -e .[dev] + +echo -e "${YELLOW}๏ฟฝ๐Ÿ” Verifying environment with pip check...${RESET}" pip check echo -e "${GREEN}โœ… Setup complete! All dependencies are installed and compatible.${RESET}" +echo -e "${GREEN}๐Ÿ“‹ Installed: Production deps (requirements.txt) + Development deps (pyproject.toml)${RESET}" diff --git a/scripts/setup.sh b/scripts/setup.sh index 1f9b54f8..d12945fc 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,11 +1,20 @@ #!/bin/bash set -e +# Complete development environment setup script for Python Basic Template +# This script sets up everything a developer needs to start working on the project: +# - Virtual environment +# - Hybrid dependency management (production + development) +# - Pre-commit hooks +# - Secret management with 1Password +# - Development tools configuration + # Colors GREEN='\033[0;32m' CYAN='\033[1;36m' YELLOW='\033[1;33m' RED='\033[0;31m' +BLUE='\033[0;34m' RESET='\033[0m' step() { @@ -25,88 +34,246 @@ error() { exit 1 } -# ------------------------------------------------------ -# ๐Ÿ” SECTION: Secret Management Setup -# ------------------------------------------------------ - -setup_secret_management() { - step "Checking system requirements..." +info() { + echo -e "${BLUE}โ„น๏ธ $1${RESET}" +} - command -v python3 >/dev/null || error "Python 3 is not installed." - command -v pip >/dev/null || error "pip is not installed." - command -v curl >/dev/null || error "curl is not installed." +section() { + echo -e "\n${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" + echo -e "${BLUE}๐Ÿ”ง $1${RESET}" + echo -e "${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" +} - # --- 1Password CLI check and setup --- - if op --version &>/dev/null; then - success "1Password CLI is installed." - else - warn "1Password CLI is installed, but not yet connected to your account in WSL." +# ------------------------------------------------------ +# ๏ฟฝ SECTION: Complete Development Environment Setup +# ------------------------------------------------------ - echo -e "${YELLOW}๐Ÿ‘‰ 1Password must be installed on Windows with CLI integration enabled, and linked to WSL.${RESET}" - echo -e "${YELLOW}Follow the instructions here:${RESET}" - echo -e "${CYAN}https://www.notion.so/techletes/Python-Basic-Template-Repository-24117d03144d809f92f9dc682d17d347${RESET}\n" - echo -e "${YELLOW}Re-run setup script when done${RESET}" - fi +setup_development_environment() { + section "SYSTEM REQUIREMENTS CHECK" + + step "Verifying required system packages..." + + info "Checking for Python 3..." + command -v python3 >/dev/null || error "Python 3 is not installed. Please install Python 3.11 or later." + PYTHON_VERSION=$(python3 --version | cut -d' ' -f2) + success "Python $PYTHON_VERSION found." + + info "Checking for pip..." + command -v pip >/dev/null || error "pip is not installed. Please install pip." + PIP_VERSION=$(pip --version | cut -d' ' -f2) + success "pip $PIP_VERSION found." + + info "Checking for curl..." + command -v curl >/dev/null || error "curl is not installed. Please install curl." + success "curl is available." + + info "Checking for git..." + command -v git >/dev/null || error "git is not installed. Please install git." + GIT_VERSION=$(git --version | cut -d' ' -f3) + success "git $GIT_VERSION found." - # Virtual environment + section "VIRTUAL ENVIRONMENT SETUP" + step "Creating Python virtual environment..." + if [ -d "venv" ]; then + warn "Virtual environment already exists. Removing old one..." + rm -rf venv + fi + python3 -m venv venv source venv/bin/activate - success "Virtual environment activated." + success "Virtual environment created and activated." + + step "Upgrading pip in virtual environment..." + pip install --upgrade pip + PIP_NEW_VERSION=$(pip --version | cut -d' ' -f2) + success "pip upgraded to version $PIP_NEW_VERSION." - # Dev dependencies - step "Setting up development dependencies" + section "HYBRID DEPENDENCY MANAGEMENT" + + info "This project uses a hybrid dependency approach:" + info "โ€ข Production dependencies: requirements.in โ†’ requirements.txt (via pip-tools)" + info "โ€ข Development dependencies: pyproject.toml [project.optional-dependencies.dev]" + + step "Installing dependency management tools..." + pip install pip-tools build wheel + success "pip-tools, build, and wheel installed." + + step "Running dependency setup script..." ./scripts/dependency.sh + success "All dependencies installed successfully." + + step "Verifying dependency installation..." + pip list --format=columns + success "Dependency verification complete." + + section "SYSTEM LOCALE CONFIGURATION" + + step "Updating system locales..." + sudo apt update + sudo apt install -y locales + sudo locale-gen en_US.UTF-8 + sudo update-locale LANG=en_US.UTF-8 + success "System locales configured." - # Install direnv - step "Installing direnv..." + section "DIRENV SETUP" + + step "Installing direnv for environment variable management..." sudo apt install -y direnv - success "Direnv installed." + success "direnv installed." - # Add direnv hook to shell profile + step "Configuring direnv shell integration..." PROFILE="${HOME}/.bashrc" if ! grep -q 'direnv hook bash' "$PROFILE"; then echo 'eval "$(direnv hook bash)"' >> "$PROFILE" success "Added direnv hook to $PROFILE." else - warn "Direnv hook already present in $PROFILE." + warn "direnv hook already present in $PROFILE." fi - # Reload shell config - step "Reloading your shell config..." + step "Reloading shell configuration..." source "$PROFILE" + success "Shell configuration reloaded." - # Direnv allow in project root step "Enabling direnv for this project..." - direnv allow || warn "Make sure .envrc exists and is correct." + if [ -f ".envrc" ]; then + direnv allow + success "direnv enabled for this project." + else + warn ".envrc file not found. direnv configuration may be incomplete." + fi - # Install pre-commit hooks + section "PRE-COMMIT HOOKS SETUP" + step "Installing pre-commit hooks..." if [[ -f ".pre-commit-config.yaml" ]]; then source venv/bin/activate pre-commit install + success "Pre-commit hooks installed." + + step "Updating pre-commit hook repositories..." pre-commit autoupdate - success "Pre-commit hooks installed and updated." + success "Pre-commit hooks updated to latest versions." + + step "Running pre-commit on all files (initial check)..." + pre-commit run --all-files || warn "Some pre-commit checks failed. This is normal for initial setup." + info "Pre-commit will now run automatically on git commits." else warn ".pre-commit-config.yaml not found. Skipping hook installation." fi - sudo apt update - sudo apt install locales - sudo locale-gen en_US.UTF-8 - sudo update-locale LANG=en_US.UTF-8 + section "SECRET MANAGEMENT SETUP" + + step "Checking 1Password CLI integration..." + if op --version &>/dev/null; then + success "1Password CLI is installed and accessible." + + step "Testing 1Password connection..." + if op account list &>/dev/null; then + success "1Password CLI is connected to your account." + else + warn "1Password CLI is installed but not connected to your account." + info "You may need to sign in: op signin" + fi + else + warn "1Password CLI not found." + info "1Password CLI must be installed on Windows with CLI integration enabled and linked to WSL." + info "Follow the instructions here:" + info "https://www.notion.so/techletes/Python-Basic-Template-Repository-24117d03144d809f92f9dc682d17d347" + info "Re-run this setup script after installing 1Password CLI." + fi - # Set baseline secrets - step "Setting baseline secrets with detect-secrets..." + step "Setting up secrets baseline with detect-secrets..." + if [ -f ".secrets.baseline" ]; then + warn "Existing .secrets.baseline found. Backing up..." + cp .secrets.baseline .secrets.baseline.backup + fi + detect-secrets scan > .secrets.baseline - success "Baseline secrets set in .secrets.baseline." + success "Secrets baseline created in .secrets.baseline." + + step "Verifying secrets configuration..." + detect-secrets audit .secrets.baseline + success "Secrets configuration verified." - echo -e "\n${GREEN}๐Ÿš€ Secret management setup complete. Happy coding!${RESET}" - echo -e "\n${GREEN}๐Ÿš€ To test if the 1Password integration was succesfull try running 'example/using_secrets.py'${RESET}" + section "DEVELOPMENT TOOLS VERIFICATION" + + step "Verifying installed development tools..." + + info "Checking Black (code formatter)..." + black --version + success "Black is working." + + info "Checking isort (import sorter)..." + isort --version + success "isort is working." + + info "Checking flake8 (linter)..." + flake8 --version + success "flake8 is working." + + info "Checking mypy (type checker)..." + mypy --version + success "mypy is working." + + info "Checking pytest (test runner)..." + pytest --version + success "pytest is working." + + section "PROJECT VALIDATION" + + step "Running final project validation..." + + info "Checking Python imports..." + python -c "import utils.secrets; import utils.utils; print('โœ… All project modules import successfully')" + + info "Running syntax check on all Python files..." + find . -name "*.py" -not -path "./venv/*" -exec python -m py_compile {} \; + success "All Python files have valid syntax." + + if [ -f "requirements.txt" ] && [ -s "requirements.txt" ]; then + info "Verifying production dependencies..." + pip check + success "All dependencies are compatible." + else + info "No production dependencies to verify (requirements.txt is empty)." + fi + + section "SETUP COMPLETE" + + echo -e "\n${GREEN}๐ŸŽ‰ DEVELOPMENT ENVIRONMENT SETUP COMPLETE! ๐ŸŽ‰${RESET}\n" + + echo -e "${BLUE}๐Ÿ“‹ What was installed:${RESET}" + echo -e " โ€ข Python virtual environment (venv/)" + echo -e " โ€ข Production dependencies (from requirements.txt)" + echo -e " โ€ข Development tools (from pyproject.toml)" + echo -e " โ€ข Pre-commit hooks for code quality" + echo -e " โ€ข Secret management with detect-secrets" + echo -e " โ€ข direnv for environment variables" + + echo -e "\n${BLUE}๐Ÿš€ Next steps:${RESET}" + echo -e " 1. Restart your terminal or run: source ~/.bashrc" + echo -e " 2. Test the setup: python example/using_secrets.py" + echo -e " 3. Start coding! The environment is ready." + + echo -e "\n${BLUE}๐Ÿ’ก Useful commands:${RESET}" + echo -e " โ€ข Activate environment: source venv/bin/activate" + echo -e " โ€ข Install new production dep: Add to requirements.in, run ./scripts/dependency.sh" + echo -e " โ€ข Install new dev tool: Add to pyproject.toml, run pip install -e .[dev]" + echo -e " โ€ข Run tests: pytest" + echo -e " โ€ข Format code: black . && isort ." + echo -e " โ€ข Type check: mypy ." + + if ! op --version &>/dev/null || ! op account list &>/dev/null; then + echo -e "\n${YELLOW}โš ๏ธ 1Password CLI setup incomplete:${RESET}" + echo -e " Install and configure 1Password CLI to use secret management features." + fi + + echo -e "\n${GREEN}Happy coding! ๐Ÿโœจ${RESET}" } # ------------------------------------------------------ # ๐Ÿ Main Execution # ------------------------------------------------------ -setup_secret_management \ No newline at end of file +setup_development_environment \ No newline at end of file From 68ec2b765952acc44537f7832683e8c118f74291 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 4 Aug 2025 15:00:14 +0200 Subject: [PATCH 009/199] Add README file --- README.md | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..640743a5 --- /dev/null +++ b/README.md @@ -0,0 +1,261 @@ +# Python Basic Template + +A modern, production-ready Python project template with hybrid dependency management, secret management, and comprehensive development tooling. + +## ๐Ÿš€ Quick Start + +### Prerequisites + +- Python 3.11 or later +- git +- curl +- sudo access (for system package installation) + +### One-Command Setup + +```bash +git clone https://github.com/thom-techlete/python_basic_template.git +cd python_basic_template +./scripts/setup.sh +``` + +This will set up everything you need for development! + +## ๐Ÿ“‹ Features + +### ๐Ÿ”ง **Modern Dependency Management** +- **Hybrid approach**: Production dependencies via `pip-tools`, development tools via `pyproject.toml` +- **Reproducible builds** with pinned versions +- **Clean separation** between production and development dependencies + +### ๐Ÿ” **Secret Management** +- **1Password CLI integration** for secure secret handling +- **detect-secrets** for preventing secrets in git +- **Environment variable management** with direnv + +### ๐Ÿ› ๏ธ **Development Tools** +- **Code formatting**: Black, isort +- **Linting**: flake8, mypy +- **Testing**: pytest with coverage +- **Pre-commit hooks** for code quality +- **Jupyter notebook support** with nbstripout + +### ๐Ÿ“ฆ **Project Structure** +``` +python_basic_template/ +โ”œโ”€โ”€ docs/ # Documentation +โ”‚ โ”œโ”€โ”€ dependency_management.md # Dependency workflow guide +โ”‚ โ””โ”€โ”€ secret_management.md # Secret management guide +โ”œโ”€โ”€ example/ # Example code +โ”‚ โ”œโ”€โ”€ using_secrets.py # Secret management examples +โ”‚ โ””โ”€โ”€ using_secrets.ipynb # Jupyter notebook examples +โ”œโ”€โ”€ scripts/ # Setup and utility scripts +โ”‚ โ”œโ”€โ”€ setup.sh # Complete development setup +โ”‚ โ”œโ”€โ”€ dependency.sh # Dependency management +โ”‚ โ””โ”€โ”€ hooks/ # Git hooks +โ”œโ”€โ”€ utils/ # Utility modules +โ”‚ โ”œโ”€โ”€ secrets.py # Secret management utilities +โ”‚ โ””โ”€โ”€ utils.py # General utilities +โ”œโ”€โ”€ .github/workflows/ # GitHub Actions +โ”œโ”€โ”€ pyproject.toml # Project configuration & dev dependencies +โ”œโ”€โ”€ requirements.in # Production dependencies +โ””โ”€โ”€ requirements.txt # Compiled production dependencies +``` + +## ๐Ÿ—๏ธ Dependency Management + +This project uses a **hybrid dependency management approach**: + +### Production Dependencies +- Add to `requirements.in` +- Compiled to `requirements.txt` via pip-tools +- Install with: `pip install -r requirements.txt` + +### Development Dependencies +- Defined in `pyproject.toml` under `[project.optional-dependencies.dev]` +- Install with: `pip install -e .[dev]` + +### Installation Options + +| Command | What it installs | +|---------|------------------| +| `pip install -e .` | Minimal (production only) | +| `pip install -e .[dev]` | Full development setup | +| `./scripts/dependency.sh` | Both production + development | + +**๐Ÿ“– Detailed guide**: [docs/dependency_management.md](docs/dependency_management.md) + +## ๐Ÿ” Secret Management + +Secure secret handling with 1Password CLI integration: + +- **Environment variables** loaded via direnv +- **1Password CLI** for secure secret retrieval +- **detect-secrets** prevents accidental commits +- **Example usage** in `example/using_secrets.py` + +**๐Ÿ“– Detailed guide**: [docs/secret_management.md](docs/secret_management.md) + +## ๐Ÿ› ๏ธ Development Workflow + +### Setup Development Environment + +```bash +# Complete setup (recommended for new developers) +./scripts/setup.sh + +# Or manual setup +python3 -m venv venv +source venv/bin/activate +pip install -e .[dev] +pre-commit install +``` + +### Adding Dependencies + +**Production dependency:** +```bash +# 1. Add to requirements.in +echo "requests" >> requirements.in + +# 2. Compile and install +./scripts/dependency.sh + +# 3. Commit both files +git add requirements.in requirements.txt +git commit -m "Add requests dependency" +``` + +**Development tool:** +```bash +# 1. Add to pyproject.toml [project.optional-dependencies.dev] +# 2. Install +pip install -e .[dev] + +# 3. Commit +git add pyproject.toml +git commit -m "Add development tool" +``` + +### Code Quality + +```bash +# Format code +black . +isort . + +# Lint code +flake8 . +mypy . + +# Run tests +pytest + +# Run all pre-commit checks +pre-commit run --all-files +``` + +## ๐Ÿงช Testing + +Run tests with pytest: + +```bash +# Run all tests +pytest + +# Run with coverage +pytest --cov + +# Run specific test +pytest tests/test_specific.py +``` + +## ๐Ÿ“ Configuration + +### Key Configuration Files + +| File | Purpose | +|------|---------| +| `pyproject.toml` | Project metadata, dev dependencies, tool config | +| `requirements.in` | Production dependencies (human-edited) | +| `requirements.txt` | Compiled production dependencies (auto-generated) | +| `.pre-commit-config.yaml` | Pre-commit hooks configuration | +| `.envrc` | Environment variables (direnv) | +| `.secrets.baseline` | Secret detection baseline | + +### Tool Configuration + +All development tools are configured in `pyproject.toml`: +- **Black**: Code formatting (88 char line length) +- **isort**: Import sorting (Black compatible) +- **pytest**: Test configuration with coverage +- **mypy**: Type checking +- **flake8**: Linting rules + +## ๐Ÿš€ Using This Template + +### For New Projects + +1. **Use this template** on GitHub or clone it +2. **Update project metadata** in `pyproject.toml`: + - Change `name`, `description`, `authors` + - Update repository URLs +3. **Run setup**: `./scripts/setup.sh` +4. **Start coding**! + +### Customization + +- **Add your production dependencies** to `requirements.in` +- **Modify tool configurations** in `pyproject.toml` +- **Update documentation** in `docs/` +- **Add your modules** alongside `utils/` + +## ๐Ÿ“š Documentation + +- **[Dependency Management](docs/dependency_management.md)** - Detailed dependency workflow +- **[Secret Management](docs/secret_management.md)** - Secure secret handling guide + +## ๐Ÿค Contributing + +1. **Setup development environment**: `./scripts/setup.sh` +2. **Create feature branch**: `git checkout -b feature/my-feature` +3. **Make changes** and ensure tests pass +4. **Pre-commit hooks** will run automatically +5. **Submit pull request** + +## ๐Ÿ“„ License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## ๐Ÿ›Ÿ Troubleshooting + +### Common Issues + +**Virtual environment issues:** +```bash +rm -rf venv +python3 -m venv venv +source venv/bin/activate +./scripts/dependency.sh +``` + +**Pre-commit hook failures:** +```bash +pre-commit run --all-files +# Fix any issues and re-commit +``` + +**1Password CLI not working:** +- Ensure 1Password CLI is installed on Windows +- Enable CLI integration in 1Password settings +- Link to WSL if using Windows Subsystem for Linux + +**Dependency conflicts:** +```bash +pip check # Identify conflicts +# Review and resolve in requirements.in or pyproject.toml +``` + +--- + +**๐ŸŽฏ Ready to start your Python project with modern tooling and best practices!** From 9fc6148b56ded404c672af92535d85c750a11f2d Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 4 Aug 2025 15:13:35 +0200 Subject: [PATCH 010/199] Add auto-formatting hooks for Python code using Black and isort --- .pre-commit-config.yaml | 34 ++++++++++++++++--------------- example/using_secrets.py | 5 ++--- scripts/hooks/black-autoformat.sh | 29 ++++++++++++++++++++++++++ scripts/hooks/isort-autoformat.sh | 29 ++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 19 deletions(-) create mode 100755 scripts/hooks/black-autoformat.sh create mode 100755 scripts/hooks/isort-autoformat.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5073ba9c..8eb46c70 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,23 @@ repos: types: [jupyter] additional_dependencies: [nbstripout] + # Auto-format Python code with Black + - id: black-autoformat + name: Auto-format Python with Black + entry: scripts/hooks/black-autoformat.sh + language: python + additional_dependencies: [black] + types: [python] + + # Auto-format Python imports with isort + - id: isort-autoformat + name: Auto-sort imports with isort + entry: scripts/hooks/isort-autoformat.sh + language: python + additional_dependencies: [isort] + types: [python] + + # Secrets scanning - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 @@ -29,19 +46,4 @@ repos: hooks: - id: jupytext args: [--sync] - require_serial: true - - # Code formatting: Black - - repo: https://github.com/psf/black - rev: 25.1.0 - hooks: - - id: black - language_version: python3 - - # Import sorting: isort (Black compatible) - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - args: ["--profile=black"] - language_version: python3 + require_serial: true \ No newline at end of file diff --git a/example/using_secrets.py b/example/using_secrets.py index 45a36736..060328e9 100644 --- a/example/using_secrets.py +++ b/example/using_secrets.py @@ -1,10 +1,9 @@ - from utils.secrets import get_secret -# Use the get_secret function to retrieve your secret. +# Use the get_secret function to retrieve your secret. # To find the path to your secret go to 1Password Interace > your item > click dropdown next to field > "Copy secret reference" api_key = get_secret("op://Shared with all/RAGFLow API Key/credential") if api_key: print(f"API_KEY is set: {api_key}") else: - print("API_KEY is not set.") \ No newline at end of file + print("API_KEY is not set.") # test diff --git a/scripts/hooks/black-autoformat.sh b/scripts/hooks/black-autoformat.sh new file mode 100755 index 00000000..74b4d870 --- /dev/null +++ b/scripts/hooks/black-autoformat.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +FILES=("$@") + +if [ ${#FILES[@]} -eq 0 ]; then + echo "[black-autoformat] No Python files staged for formatting." + exit 0 +fi + +# Run Black on provided files +black --quiet "${FILES[@]}" + +# Re-add any changed files +CHANGED=0 +for file in "${FILES[@]}"; do + if ! git diff --quiet -- "$file"; then + git add "$file" + echo "[black-autoformat] Reformatted and re-staged: $file" + CHANGED=1 + fi +done + +# Optional: notify user to re-check commit +if [ $CHANGED -eq 1 ]; then + echo "[black-autoformat] Formatting applied. Review changes before final commit." +fi + +exit 0 diff --git a/scripts/hooks/isort-autoformat.sh b/scripts/hooks/isort-autoformat.sh new file mode 100755 index 00000000..ea90bca8 --- /dev/null +++ b/scripts/hooks/isort-autoformat.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +FILES=("$@") + +if [ ${#FILES[@]} -eq 0 ]; then + echo "[isort-autoformat] No Python files staged for sorting." + exit 0 +fi + +# Run isort with Black compatibility +isort --profile=black "${FILES[@]}" + +# Re-add any changed files +CHANGED=0 +for file in "${FILES[@]}"; do + if ! git diff --quiet -- "$file"; then + git add "$file" + echo "[isort-autoformat] Sorted imports and re-staged: $file" + CHANGED=1 + fi +done + +# Optional: notify user to re-check commit +if [ $CHANGED -eq 1 ]; then + echo "[isort-autoformat] Imports sorted. Review changes before final commit." +fi + +exit 0 From 28ed89edf8f6cf78d5676ffd202ca8c201343a96 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 4 Aug 2025 15:14:20 +0200 Subject: [PATCH 011/199] test --- example/using_secrets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/using_secrets.py b/example/using_secrets.py index 060328e9..8edbe43c 100644 --- a/example/using_secrets.py +++ b/example/using_secrets.py @@ -6,4 +6,4 @@ if api_key: print(f"API_KEY is set: {api_key}") else: - print("API_KEY is not set.") # test + print("API_KEY is not set.") From 84d4fd85f53f5d4cefd4b189a80b1bd39366d2f5 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 4 Aug 2025 17:17:03 +0200 Subject: [PATCH 012/199] Add ruff and specify python => 3.12 --- .pre-commit-config.yaml | 10 +++++++- README.md | 2 +- pyproject.toml | 54 ++++++++++++++++++++--------------------- scripts/setup.sh | 2 +- 4 files changed, 37 insertions(+), 31 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8eb46c70..2fb517e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,4 +46,12 @@ repos: hooks: - id: jupytext args: [--sync] - require_serial: true \ No newline at end of file + require_serial: true + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.4 + hooks: + - id: ruff + name: Ruff (linting + extra isort + rules) + args: [--fix] + types: [python] diff --git a/README.md b/README.md index 640743a5..410a59fa 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A modern, production-ready Python project template with hybrid dependency manage ### Prerequisites -- Python 3.11 or later +- Python 3.12 or later - git - curl - sudo access (for system package installation) diff --git a/pyproject.toml b/pyproject.toml index 5f113cba..87742dab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ name = "python-basic-template" version = "0.1.0" description = "A basic Python project template" readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.12" license = {text = "MIT"} authors = [ {name = "Your Name", email = "your.email@example.com"}, @@ -20,7 +20,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] dependencies = [] @@ -33,9 +32,8 @@ dev = [ "pytest", "pytest-cov", "black", - "isort", - "flake8", "mypy", + "ruff", "pip-tools", # for managing requirements.in/txt ] @@ -51,16 +49,7 @@ exclude = ["tests*"] [tool.black] line-length = 88 -target-version = ["py311"] - -[tool.isort] -profile = "black" -line_length = 88 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true -ensure_newline_before_comments = true +target-version = ["py312"] [tool.pytest.ini_options] minversion = "6.0" @@ -102,7 +91,7 @@ exclude_lines = [ ] [tool.mypy] -python_version = "3.11" +python_version = "3.12" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true @@ -117,23 +106,32 @@ warn_unreachable = true strict_equality = true show_error_codes = true +[tool.ruff] +target-version = "py311" +line-length = 88 +fix = true +respect-gitignore = true +extend-exclude = ["migrations"] + +# Enable popular rule sets +select = [ + "E", # pycodestyle + "F", # pyflakes + "I", # isort + "B", # bugbear + "UP", # pyupgrade +] + +# Rules to ignore (common ones) +ignore = [ + "B008", # Do not perform function calls in argument defaults + "E501", # Line too long (Black handles this) +] + [[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false -[tool.flake8] -max-line-length = 88 -extend-ignore = ["E203", "W503"] -exclude = [ - ".git", - "__pycache__", - ".venv", - "venv", - "build", - "dist", - "*.egg-info", -] - [tool.bandit] exclude_dirs = ["tests", "venv"] skips = ["B101", "B601"] diff --git a/scripts/setup.sh b/scripts/setup.sh index d12945fc..b86c61bc 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -54,7 +54,7 @@ setup_development_environment() { step "Verifying required system packages..." info "Checking for Python 3..." - command -v python3 >/dev/null || error "Python 3 is not installed. Please install Python 3.11 or later." + command -v python3 >/dev/null || error "Python 3 is not installed. Please install Python 3.12 or later." PYTHON_VERSION=$(python3 --version | cut -d' ' -f2) success "Python $PYTHON_VERSION found." From bc10ffc2e1493a7f211a75c176d531e011edc224 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 4 Aug 2025 17:17:50 +0200 Subject: [PATCH 013/199] Refactor code for improved readability and organization by rearranging import statements and formatting error messages. --- .pre-commit-config.yaml | 8 -------- utils/secrets.py | 10 +++++++--- utils/utils.py | 5 ++++- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2fb517e6..ed96eb35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,14 +24,6 @@ repos: additional_dependencies: [black] types: [python] - # Auto-format Python imports with isort - - id: isort-autoformat - name: Auto-sort imports with isort - entry: scripts/hooks/isort-autoformat.sh - language: python - additional_dependencies: [isort] - types: [python] - # Secrets scanning - repo: https://github.com/Yelp/detect-secrets diff --git a/utils/secrets.py b/utils/secrets.py index a7eb7b6c..57e3b6a5 100644 --- a/utils/secrets.py +++ b/utils/secrets.py @@ -1,8 +1,10 @@ # A tiny helper module to load secrets at runtime if you canโ€™t rely on direnv # This uses the 1Password CLI to fetch secrets on-the-fly in any script or app -import subprocess, re import os +import re +import subprocess + def get_secret(path: str, env_var: str = None) -> str: """ @@ -22,8 +24,10 @@ def get_secret(path: str, env_var: str = None) -> str: # Validate path format: op://Vault name/Item name/field pattern = r"^op://[^/]+/[^/]+/[^/]+$" if not re.match(pattern, path): - raise ValueError("Secret path must be of the form 'op://Vault name/Item name/field'") - secret = subprocess.check_output(['op', 'read', path]).decode().strip() + raise ValueError( + "Secret path must be of the form 'op://Vault name/Item name/field'" + ) + secret = subprocess.check_output(["op", "read", path]).decode().strip() if env_var: os.environ[env_var] = secret return secret diff --git a/utils/utils.py b/utils/utils.py index 29392ba7..2e14246a 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -1,10 +1,13 @@ import sys from pathlib import Path + def add_project_root(marker: str = ".git") -> Path: path = Path(__file__).resolve() for parent in path.parents: if (parent / marker).exists(): sys.path.append(str(parent)) return - raise RuntimeError(f"Project root not found (no '{marker}' in any parent directories)") + raise RuntimeError( + f"Project root not found (no '{marker}' in any parent directories)" + ) From 2ba8324aa11eb5f5470ac990af86488d5e3d258b Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 4 Aug 2025 17:19:29 +0200 Subject: [PATCH 014/199] Remove isort autoformat hook script --- scripts/hooks/isort-autoformat.sh | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100755 scripts/hooks/isort-autoformat.sh diff --git a/scripts/hooks/isort-autoformat.sh b/scripts/hooks/isort-autoformat.sh deleted file mode 100755 index ea90bca8..00000000 --- a/scripts/hooks/isort-autoformat.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -FILES=("$@") - -if [ ${#FILES[@]} -eq 0 ]; then - echo "[isort-autoformat] No Python files staged for sorting." - exit 0 -fi - -# Run isort with Black compatibility -isort --profile=black "${FILES[@]}" - -# Re-add any changed files -CHANGED=0 -for file in "${FILES[@]}"; do - if ! git diff --quiet -- "$file"; then - git add "$file" - echo "[isort-autoformat] Sorted imports and re-staged: $file" - CHANGED=1 - fi -done - -# Optional: notify user to re-check commit -if [ $CHANGED -eq 1 ]; then - echo "[isort-autoformat] Imports sorted. Review changes before final commit." -fi - -exit 0 From d2e415d3ae61a838aa061a23e0a16598bc0cf075 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 4 Aug 2025 17:28:26 +0200 Subject: [PATCH 015/199] Add mypy type checking and update ruff version in pre-commit config --- .pre-commit-config.yaml | 10 +++++++++- pyproject.toml | 22 ++++++++++------------ utils/secrets.py | 2 +- utils/utils.py | 2 +- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed96eb35..369ecf4e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,8 +40,16 @@ repos: args: [--sync] require_serial: true + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.17.1 + hooks: + - id: mypy + name: Type check with mypy + args: ["--config-file=pyproject.toml"] + + - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.4 + rev: v0.12.7 hooks: - id: ruff name: Ruff (linting + extra isort + rules) diff --git a/pyproject.toml b/pyproject.toml index 87742dab..df29a640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,19 +92,17 @@ exclude_lines = [ [tool.mypy] python_version = "3.12" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -disallow_untyped_decorators = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_no_return = true -warn_unreachable = true -strict_equality = true +strict = true +ignore_missing_imports = true show_error_codes = true +warn_unused_ignores = true +show_column_numbers = true +pretty = true +exclude = [ + "migrations", # if Django/SQLAlchemy used + "notebooks", # if you sync with Jupytext +] +plugins = [] [tool.ruff] target-version = "py311" diff --git a/utils/secrets.py b/utils/secrets.py index 57e3b6a5..ed738db4 100644 --- a/utils/secrets.py +++ b/utils/secrets.py @@ -6,7 +6,7 @@ import subprocess -def get_secret(path: str, env_var: str = None) -> str: +def get_secret(path: str, env_var: str | None = None) -> str: """ Fetch a secret from 1Password CLI and optionally set it as an environment variable. diff --git a/utils/utils.py b/utils/utils.py index 2e14246a..7aa1b998 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -2,7 +2,7 @@ from pathlib import Path -def add_project_root(marker: str = ".git") -> Path: +def add_project_root(marker: str = ".git") -> None: path = Path(__file__).resolve() for parent in path.parents: if (parent / marker).exists(): From dfa1219545905424adc23993f2289c4dad2bb0aa Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 4 Aug 2025 18:01:50 +0200 Subject: [PATCH 016/199] Update .gitignore, add VSCode settings, and implement runtime type checking with beartype --- .gitignore | 4 +++- .vscode/settings.json | 8 ++++++++ example/__init__.py | 6 ++++++ pyproject.toml | 3 +++ utils/__init__.py | 6 ++++++ 5 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 01661b5e..298ceac1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ ENV/ .ipynb_checkpoints/ # VSCode settings -.vscode/ +# .vscode/ # PyCharm settings .idea/ @@ -26,6 +26,8 @@ ENV/ .dmypy.json dmypy.json +.ruff_cache/ + # Pyre type checker .pyre/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e5a1cafb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.analysis.typeCheckingMode": "strict", + "python.linting.mypyEnabled": true, + "python.linting.enabled": true, + "python.linting.mypyArgs": ["--config-file=pyproject.toml"], + "ruff.enable": true, + "ruff.args": ["--config=pyproject.toml"], +} diff --git a/example/__init__.py b/example/__init__.py index e69de29b..1f1a0509 100644 --- a/example/__init__.py +++ b/example/__init__.py @@ -0,0 +1,6 @@ +import os + +from beartype.claw import beartype_this_package + +if os.getenv("RUNTIME_TYPECHECK", "true") == "true": + beartype_this_package() diff --git a/pyproject.toml b/pyproject.toml index df29a640..8acad03a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,8 @@ dev = [ "black", "mypy", "ruff", + "beartype", + "pytest-beartype", "pip-tools", # for managing requirements.in/txt ] @@ -61,6 +63,7 @@ addopts = [ "--cov-report=term-missing", "--cov-report=html", "--cov-report=xml", + "--beartype-packages=utils,example", ] testpaths = ["tests"] python_files = ["test_*.py", "*_test.py"] diff --git a/utils/__init__.py b/utils/__init__.py index e69de29b..1f1a0509 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -0,0 +1,6 @@ +import os + +from beartype.claw import beartype_this_package + +if os.getenv("RUNTIME_TYPECHECK", "true") == "true": + beartype_this_package() From 66a9c4939e92235635a4dd2360dd3763d9020200 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 4 Aug 2025 18:17:42 +0200 Subject: [PATCH 017/199] Refactor documentation: consolidate and enhance dependency and secret management sections, add pre-commit hook setup, and improve code quality guidelines. --- ...t_management.md => 1_secret_management.md} | 0 ...nagement.md => 2_dependency_management.md} | 0 docs/3_pre_commit_hooks.md | 186 ++++++++++++++++++ docs/4_code_quality.md | 141 +++++++++++++ docs/progress.md | 109 ++++++++++ 5 files changed, 436 insertions(+) rename docs/{secret_management.md => 1_secret_management.md} (100%) rename docs/{dependency_management.md => 2_dependency_management.md} (100%) create mode 100644 docs/3_pre_commit_hooks.md create mode 100644 docs/4_code_quality.md create mode 100644 docs/progress.md diff --git a/docs/secret_management.md b/docs/1_secret_management.md similarity index 100% rename from docs/secret_management.md rename to docs/1_secret_management.md diff --git a/docs/dependency_management.md b/docs/2_dependency_management.md similarity index 100% rename from docs/dependency_management.md rename to docs/2_dependency_management.md diff --git a/docs/3_pre_commit_hooks.md b/docs/3_pre_commit_hooks.md new file mode 100644 index 00000000..0a00d06c --- /dev/null +++ b/docs/3_pre_commit_hooks.md @@ -0,0 +1,186 @@ +# 3. Pre-commit Hook Setup + +This project uses `pre-commit` to automate code quality checks and housekeeping tasks such as secret detection, notebook cleanup, and dependency enforcement before code is committed. + +All hooks run **locally** and must pass before a commit is allowed. + +--- + +## 3.1 Overview of Hooks + +The following hooks are currently configured: + +| Hook ID | Purpose | +| --- | --- | +| `prevent-manual-requirements-edits` | Block manual edits to `requirements.txt` | +| `nbstripout-autoadd` | Strip notebook outputs and auto-stage cleaned files | +| `black-autoformat` | Auto-format Python code with Black | +| `detect-secrets` | Detect and prevent committing secrets | +| `jupytext` | Sync `.ipynb` and `.py` notebook pairs | +| `mypy` | Type check Python code with mypy | +| `ruff` | Lint Python code with Ruff (includes isort) | + +The configuration is stored in the `.pre-commit-config.yaml` file at the project root. + +--- + +--- + +## 3.2 How to Use + +From now on, every time you run `git commit`, `pre-commit` will: + +1. Block manual edits to `requirements.txt` +2. Strip outputs from notebooks using `nbstripout-autoadd` +3. Auto-format Python code with Black +4. Detect any committed secrets with `detect-secrets` +5. Sync `.ipynb` and `.py` files using `jupytext` +6. Type check Python code with `mypy` +7. Lint Python code with `ruff` (includes import sorting) + +If any hook fails, the commit is blocked until the issue is resolved. + +--- + +## 3.3 Explanation of hooks + +### 3.3.1 Auto-Staging of Notebooks + +The `nbstripout-autoadd` hook is a **local wrapper** that: + +- Strips outputs from Jupyter notebooks +- Detects which `.ipynb` files were modified +- Automatically stages those cleaned files so you donโ€™t need to run `git add` manually + +--- + +### 3.3.2 Code Formatting and Quality + +#### Black Auto-formatting + +The `black-autoformat` hook automatically formats Python code to ensure consistent style across the project. Black is an opinionated code formatter that: + +- Ensures consistent indentation and spacing +- Formats imports and function definitions +- Handles line length and string formatting +- Automatically fixes formatting issues + +If Black makes changes to your files, they will be automatically staged for commit. + +#### Type Checking with MyPy + +The `mypy` hook performs static type checking on Python code to catch type-related errors before runtime. It: + +- Validates type annotations +- Catches potential type mismatches +- Enforces type safety +- Uses configuration from `pyproject.toml` + +#### Linting with Ruff + +The `ruff` hook provides fast Python linting and includes: + +- Code quality checks (similar to flake8) +- Import sorting (replacing isort) +- Security vulnerability detection +- Performance and bug detection +- Automatically fixes issues where possible + +--- + +### 3.3.3 Keeping Secrets Safe + +To avoid accidentally committing secrets (API keys, tokens, etc.), the project uses `detect-secrets`. This works by scanning committed files for sensitive content using a predefined baseline. + +**Updating the secret baseline** + +If you intentionally add or rotate secrets: + +```bash +detect-secrets scan > .secrets.baseline +git add .secrets.baseline +``` + +Make sure `.secrets.baseline` stays committed and up to date. + +--- + +### 3.3.4 Enforcing Dependency Workflow + +The custom `prevent-manual-requirements-edits` hook ensures that `requirements.txt` is only modified via `pip-compile` and never edited manually. + +To add a package: + +```bash +echo "package-name" >> requirements.in +./scripts/dependency.sh +``` + +--- + +## 3.4 Running All Hooks Manually + +To check all files before pushing or after changing hook config: + +```bash +pre-commit run --all-files +``` + +This is useful to verify changes or fix all issues in one go. + +--- + +## 3.5 Troubleshooting + +### 3.5.1 Locale warnings (WSL or CI environments) + +- If you see: + + ```bash + setlocale: LC_ALL: cannot change locale (en_US.UTF-8) + ``` + +- Fix it by running: + + ```bash + sudo apt install locales + sudo locale-gen en_US.UTF-8 + sudo update-locale LANG=en_US.UTF-8 + ``` + + +### 3.5.2 `nbstripout: command not found` + +This means `nbstripout` isnโ€™t installed in the hookโ€™s environment. Make sure your `.pre-commit-config.yaml` includes: + +```yaml + additional_dependencies: [nbstripout] +``` + +in the `nbstripout-autoadd` hook definition. + +### 3.5.3 MyPy Type Checking Issues + +If you encounter type checking errors: + +- Add type annotations to your functions and variables +- Use `# type: ignore` comments for specific lines that can't be typed +- Update your `pyproject.toml` mypy configuration if needed +- Install type stubs for third-party packages: `pip install types-` + +### 3.5.4 Ruff Linting Issues + +If Ruff reports linting errors: + +- Most issues can be auto-fixed by running: `ruff check --fix .` +- Check the specific error codes and adjust your code accordingly +- Use `# noqa: ` to ignore specific violations +- Update the Ruff configuration in `pyproject.toml` if needed + +### 3.5.5 Black Formatting Conflicts + +If you have formatting conflicts: + +- Let Black handle all formatting automatically +- Avoid manual formatting that conflicts with Black's style +- Use `# fmt: off` and `# fmt: on` comments to disable Black for specific sections if absolutely necessary \ No newline at end of file diff --git a/docs/4_code_quality.md b/docs/4_code_quality.md new file mode 100644 index 00000000..47ca7399 --- /dev/null +++ b/docs/4_code_quality.md @@ -0,0 +1,141 @@ +# 4. Code Quality + +To ensure robust, maintainable, and secure Python code across all TECHLETES projects, this template integrates a layered code quality system focused on two key areas: **typing** and **quality enforcement**. The tools used are Ruff, Mypy, Beartype, and Black. + +## 4.1 Typing + +Typing tools ensure correctness before and during runtime by statically analyzing annotated types and optionally enforcing them during execution. + +### 4.1.1 Mypy (Static Type Checking) + +Mypy checks type correctness at development and commit time, catching bugs before runtime. + +- Runs on staged files via pre-commit +- Full project checks can be done with `pre-commit run --all-files` or in CI +- Integrated into editors like VS Code for real-time feedback +- Configured in `pyproject.toml` + +**Run manually:** + +```bash +mypy . --config-file=pyproject.toml +``` + +**What it checks:** + +- Incorrect argument or return types +- Type mismatches across modules +- Missing annotations + +### 4.1.2 Beartype (Runtime Type Enforcement) + +Beartype enforces type correctness at runtime for annotated functions and classes. + +- Enabled automatically in development and testing (when `RUNTIME_TYPECHECK=true`) +- Controlled by `RUNTIME_TYPECHECK` environment variable (defaults to `true`) +- Can be disabled in production by setting `RUNTIME_TYPECHECK=false` for performance +- Automatically imports and instruments packages via `beartype_this_package()` in `__init__.py` files + +**Test integration:** + +Beartype is automatically enabled during pytest execution through the `--beartype-packages` configuration in `pyproject.toml`: + +```bash +pytest # Beartype is automatically enabled for utils and example packages +``` + +To run tests without beartype (for performance): + +```bash +RUNTIME_TYPECHECK=false pytest +``` + +## 4.2 Quality + +Quality tools enforce style, consistency, and early bug detection. Ruff, Black, and other pre-commit hooks are used for automatic linting and formatting. + +### 4.2.1 Ruff (Linting and Import Sorting) + +Ruff provides fast linting and catches many code issues before commit. + +- Replaces flake8, isort, and pyupgrade in a single fast tool +- Configured via `pyproject.toml` with enabled rules: E (pycodestyle), F (pyflakes), I (isort), B (bugbear), UP (pyupgrade) +- Automatically fixes issues when possible +- Target version set to Python 3.11+ with 88-character line length + +**Run manually:** + +```bash +ruff check . --fix +``` + +**What it catches:** + +- Unused imports or variables +- Syntax errors +- Import ordering and formatting violations +- Code smells (e.g. using `lambda x: x` unnecessarily) + +### 4.2.2 Black (Code Formatting) + +Black is the opinionated formatter used to ensure all code is styled consistently. + +- Used alongside Ruff for full formatting consistency +- Auto-formats code during pre-commit hooks +- Configured in `pyproject.toml` with 88-character line length and Python 3.12 target +- Integrated with the pre-commit `black-autoformat` hook + +**Run manually:** + +```bash +black . +``` + +**What it does:** + +- Enforces line length and consistent formatting +- Normalizes string quotes and indentation +- Removes formatting inconsistencies across teams + +## 4.3 Configuration Details + +All tools are configured in `pyproject.toml`: + +- **MyPy**: Strict mode enabled with Python 3.12 target, excludes migrations and notebooks +- **Ruff**: Targets Python 3.11+ with rules E, F, I, B, UP enabled, respects .gitignore +- **Black**: 88-character line length, Python 3.12 target version +- **Beartype**: Automatically enabled for `utils` and `example` packages during testing +- **Pytest**: Configured with coverage reporting and beartype integration + +## 4.4 Summary + +| Tool | Purpose | When It Runs | Configuration | +| --- | --- | --- | --- | +| Ruff | Linting & import sorting | Pre-commit, CI, manual | `pyproject.toml` - rules E,F,I,B,UP | +| Black | Code formatting | Pre-commit, manual | `pyproject.toml` - 88 chars, py312 | +| MyPy | Static type checking | Pre-commit, CI, IDE | `pyproject.toml` - strict mode | +| Beartype | Runtime type checking | Dev/test execution | `RUNTIME_TYPECHECK` env var | + +## 4.5 Running All Tools + +To run all quality checks manually: + +```bash +# Format code +black . + +# Lint and fix issues +ruff check . --fix + +# Type check +mypy . + +# Run tests with beartype +pytest +``` + +Or use pre-commit to run all configured hooks: + +```bash +pre-commit run --all-files +``` \ No newline at end of file diff --git a/docs/progress.md b/docs/progress.md new file mode 100644 index 00000000..a015f940 --- /dev/null +++ b/docs/progress.md @@ -0,0 +1,109 @@ +# 1. Current Implementation Overview + +This template repository already includes the following DevOps foundations for Python projects: + +## 1.1 Secrets Management + +* 1Password CLI + `direnv` integration for dynamic secret loading +* `detect-secrets` preโ€‘commit hook with `.secrets.baseline` +* `SECURITY.md` playbook linking to incident response steps + +## 1.2 Dependency Management + +* `requirements.in` / `requirements.txt` pinned via `pipโ€‘tools` +* Preโ€‘commit `prevent-manual-requirements-edits` hook + +## 1.3 Code Quality + +* **Ruff**: linting, import sorting, autoโ€‘fix (`--fix`) via preโ€‘commit +* **Black**: opinionated code formatting via preโ€‘commit +* **Mypy**: static type checking on staged files (preโ€‘commit) and full checks in CI +* **Beartype**: runtime type enforcement in dev/test environments (toggle via `RUNTIME_TYPECHECK`) + +## 1.4 Preโ€‘commit Infrastructure + +* Local hooks: + + * `prevent-manual-requirements-edits` + * `nbstripout-autoadd` + * `black-autoformat` (autoโ€‘format + reโ€‘stage) + * `pytest-runtime-types` (optional) +* External hooks: + + * `mirrors-mypy` + * `ruff-pre-commit` + * `psf/black` + +## 1.5 CI/CD & Repository Settings + +* GitHub Actions workflows enforcing preโ€‘commit hooks and fullโ€‘repo checks +* Organizationโ€‘level branch protection rulesets applied + +## 1.6 Editor & Test Integration + +* VSย Code configured for live Mypy feedback +* `pytest-beartype` plugin for runtime checks in tests + +# 2. Toโ€‘Dos for a Bestโ€‘inโ€‘Class DevOps Strategy + +Below are subjectโ€‘specific action items to further enhance our DevOps maturity. + +## 2.1 Secrets Management + +* Add automated secret rotation scan and enforcement in CI (e.g., 1Password rotation policy check) +* Integrate **`pip-audit`** or **`safety`** for dependency vulnerability scanning +* Evaluate centralized secrets store (e.g., Vault) for dynamic credentials in CI/CD +* Enforce environmentโ€‘specific secret vaults (dev, staging, prod) + +## 2.2 Dependency Management + +* Add a CI job to validate `requirements.txt` against `requirements.in` (`pip-compile --dry-run`) +* Enable Dependabot or Renovate to automate dependency updates +* Consider adopting **PEPย 621** metadata in `pyproject.toml` for standardized packaging + +## 2.3 Code Quality + +* Enforce **test coverage thresholds** (e.g., `pytest --cov`, coverage badge) +* Introduce **commit message linting** (e.g., conventional commits with `commitlint`) +* Add **CODEOWNERS** and require reviews from code owners in branch rules + +## 2.4 CI/CD + +* Implement **matrix testing** across multiple Python versions in GitHub Actions +* Add a staging deployment workflow to automatically build and test a Docker image +* Integrate **release automation** (autoโ€‘generate changelogs, tag releases) +* Cache dependencies and artifacts to speed up pipelines + +## 2.5 Packaging & Distribution + +* Scaffold `setup.py` / `pyproject.toml` with metadata for publishing +* Configure GitHub Packages or private PyPI for internal distribution +* Automate packaging & publish step in CI upon tag release + +## 2.6 Observability & Monitoring + +* Provide a logging and metrics template (e.g., `structlog`, Prometheus client) +* Integrate Sentry or similar error monitoring in template +* Add a healthโ€‘check endpoint stub for service readiness/liveness + +## 2.7 Infrastructure as Code + +* Include a **Dockerfile** template following best practices (multiโ€‘stage, minimal base) +* Provide Terraform or Pulumi skeleton to provision common resources (e.g., VPC, databases) +* Add a CI job to validate IaC with `terraform fmt` / `validate` + +## 2.8 Documentation & Onboarding + +* Create `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, and `README.md` badges for code quality status +* Automate documentation generation (e.g., Sphinx/MkDocs) in CI +* Provide Notion templates or a docs portal scaffold + +## 2.9 Security & Compliance + +* Add **Bandit** security scanning via preโ€‘commit or CI +* Implement license scanning to detect unapproved licenses +* Schedule periodic dependency audits and security reviews (automated reminders) + +--- + +This roadmap will help us evolve our template into a comprehensive, ISOย 27001โ€‘aligned DevOps standard for all Python projects at TECHLETES. From a5687c24fbb563b310ab72c4f13d5175db83b136 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 13:36:14 +0200 Subject: [PATCH 018/199] Unify dependency management to pyproject with pip tools --- .envrc | 15 +- .gitignore | 4 + .pre-commit-config.yaml | 2 +- .secrets.baseline | 2 +- pyproject.toml | 19 +- requirements-dev.txt | 754 ++++++++++++++++++++++++++++++++++++++++ requirements.in | 2 - requirements.txt | 201 ++++++++++- scripts/dependency.sh | 81 +++-- scripts/setup.sh | 244 +++++-------- setup.py | 8 + 11 files changed, 1139 insertions(+), 193 deletions(-) create mode 100644 requirements-dev.txt delete mode 100644 requirements.in create mode 100644 setup.py diff --git a/.envrc b/.envrc index 27996c2f..959bf71f 100644 --- a/.envrc +++ b/.envrc @@ -1,11 +1,18 @@ # Load environment variables from .env if it exists if [ -f .env ]; then - export $(grep -v '^#' .env | xargs) + set -a + source .env + set +a fi +export PYTHONPATH=. +export VENV_DIR="venv" +export PROD_LOCK="requirements.txt" +export DEV_LOCK="requirements-dev.txt" +export PYPROJECT="pyproject.toml" + # Set a virtualenv automatically if venv exists -if [ -d venv ]; then - source venv/bin/activate +if [ -d "$VENV_DIR" ]; then + source $VENV_DIR/bin/activate fi -export PYTHONPATH=. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 298ceac1..65ea155e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Don't use requirement.in or manually modified requirements.txt! +# We use pip-tools to manage dependencies. It reads and builds dpendencies from pyproject.toml. +requirements.in + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 369ecf4e..59468ae7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: # Notebook/text sync - repo: https://github.com/mwouts/jupytext - rev: v1.14.7 + rev: v1.17.2 hooks: - id: jupytext args: [--sync] diff --git a/.secrets.baseline b/.secrets.baseline index d1b8b2e3..ad4e42b3 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -123,5 +123,5 @@ } ], "results": {}, - "generated_at": "2025-08-01T13:52:27Z" + "generated_at": "2025-08-05T11:33:54Z" } diff --git a/pyproject.toml b/pyproject.toml index 8acad03a..75b43c30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,12 +8,16 @@ build-backend = "setuptools.build_meta" [project] name = "python-basic-template" version = "0.1.0" -description = "A basic Python project template" +description = "A basic Python project template with full DevOps tooling support." readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.12,<4.0" +keywords = ["devops", "template", "python"] license = {text = "MIT"} authors = [ - {name = "Your Name", email = "your.email@example.com"}, + {name = "Thom Ezechiels", email = "thom@techletes.ai"}, +] +maintainers = [ + { name = "Techletes DevOps Team", email = "dev@techletes.ai" } ] classifiers = [ "Development Status :: 3 - Alpha", @@ -22,14 +26,19 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", ] -dependencies = [] + +# Add project dependencies here. pip-tools will read them from here, do not use a requirements file! +dependencies = [ + "flask>=2.2.0,<3.0.0", + "requests>=2.28.0", +] [project.optional-dependencies] dev = [ "pre-commit", "detect-secrets", "nbstripout", - "pytest", + "pytest>=7.0", "pytest-cov", "black", "mypy", diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..82fcd913 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,754 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --extra=dev --generate-hashes --output-file=requirements-dev.txt pyproject.toml +# +attrs==25.3.0 \ + --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \ + --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b + # via + # jsonschema + # referencing +beartype==0.21.0 \ + --hash=sha256:b6a1bd56c72f31b0a496a36cc55df6e2f475db166ad07fa4acc7e74f4c7f34c0 \ + --hash=sha256:f9a5078f5ce87261c2d22851d19b050b64f6a805439e8793aecf01ce660d3244 + # via + # pytest-beartype + # python-basic-template (pyproject.toml) +black==25.1.0 \ + --hash=sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171 \ + --hash=sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7 \ + --hash=sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da \ + --hash=sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2 \ + --hash=sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc \ + --hash=sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666 \ + --hash=sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f \ + --hash=sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b \ + --hash=sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32 \ + --hash=sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f \ + --hash=sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717 \ + --hash=sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299 \ + --hash=sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0 \ + --hash=sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18 \ + --hash=sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0 \ + --hash=sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3 \ + --hash=sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355 \ + --hash=sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096 \ + --hash=sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e \ + --hash=sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9 \ + --hash=sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba \ + --hash=sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f + # via python-basic-template (pyproject.toml) +blinker==1.9.0 \ + --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ + --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc + # via flask +build==1.3.0 \ + --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ + --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 + # via pip-tools +certifi==2025.8.3 \ + --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ + --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 + # via requests +cfgv==3.4.0 \ + --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ + --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 + # via pre-commit +charset-normalizer==3.4.2 \ + --hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \ + --hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \ + --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \ + --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \ + --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \ + --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \ + --hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \ + --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \ + --hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \ + --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \ + --hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \ + --hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \ + --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \ + --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \ + --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \ + --hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \ + --hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \ + --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \ + --hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \ + --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \ + --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \ + --hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \ + --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \ + --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \ + --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \ + --hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \ + --hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \ + --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \ + --hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \ + --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \ + --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \ + --hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \ + --hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \ + --hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \ + --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \ + --hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \ + --hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \ + --hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \ + --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \ + --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \ + --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \ + --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \ + --hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \ + --hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \ + --hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \ + --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \ + --hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \ + --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \ + --hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \ + --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \ + --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \ + --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \ + --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \ + --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \ + --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \ + --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \ + --hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \ + --hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \ + --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \ + --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \ + --hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \ + --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \ + --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \ + --hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \ + --hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \ + --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \ + --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \ + --hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \ + --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \ + --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \ + --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \ + --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \ + --hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \ + --hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \ + --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \ + --hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \ + --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \ + --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \ + --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \ + --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \ + --hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \ + --hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \ + --hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \ + --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \ + --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \ + --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \ + --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \ + --hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \ + --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \ + --hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \ + --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \ + --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f + # via requests +click==8.2.1 \ + --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ + --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b + # via + # black + # flask + # pip-tools +coverage[toml]==7.10.2 \ + --hash=sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b \ + --hash=sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc \ + --hash=sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7 \ + --hash=sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba \ + --hash=sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303 \ + --hash=sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc \ + --hash=sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20 \ + --hash=sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4 \ + --hash=sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01 \ + --hash=sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a \ + --hash=sha256:22aca3e691c7709c5999ccf48b7a8ff5cf5a8bd6fe9b36efbd4993f5a36b2fcf \ + --hash=sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8 \ + --hash=sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27 \ + --hash=sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57 \ + --hash=sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3 \ + --hash=sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c \ + --hash=sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b \ + --hash=sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406 \ + --hash=sha256:44329cbed24966c0b49acb386352c9722219af1f0c80db7f218af7793d251902 \ + --hash=sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468 \ + --hash=sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb \ + --hash=sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed \ + --hash=sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf \ + --hash=sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4 \ + --hash=sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055 \ + --hash=sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b \ + --hash=sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b \ + --hash=sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7 \ + --hash=sha256:6c031da749a05f7a01447dd7f47beedb498edd293e31e1878c0d52db18787df0 \ + --hash=sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074 \ + --hash=sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7 \ + --hash=sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd \ + --hash=sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3 \ + --hash=sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288 \ + --hash=sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0 \ + --hash=sha256:765b13b164685a2f8b2abef867ad07aebedc0e090c757958a186f64e39d63dbd \ + --hash=sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518 \ + --hash=sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65 \ + --hash=sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9 \ + --hash=sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de \ + --hash=sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46 \ + --hash=sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21 \ + --hash=sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83 \ + --hash=sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824 \ + --hash=sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0 \ + --hash=sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f \ + --hash=sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b \ + --hash=sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226 \ + --hash=sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1 \ + --hash=sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0 \ + --hash=sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f \ + --hash=sha256:9bdff88e858ee608a924acfad32a180d2bf6e13e059d6a7174abbae075f30436 \ + --hash=sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be \ + --hash=sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87 \ + --hash=sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1 \ + --hash=sha256:a219b70100500d0c7fd3ebb824a3302efb6b1a122baa9d4eb3f43df8f0b3d899 \ + --hash=sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6 \ + --hash=sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95 \ + --hash=sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0 \ + --hash=sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f \ + --hash=sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186 \ + --hash=sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388 \ + --hash=sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f \ + --hash=sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340 \ + --hash=sha256:bc3945b7bad33957a9eca16e9e5eae4b17cb03173ef594fdaad228f4fc7da53b \ + --hash=sha256:be127f292496d0fbe20d8025f73221b36117b3587f890346e80a13b310712982 \ + --hash=sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322 \ + --hash=sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1 \ + --hash=sha256:c7195444b932356055a8e287fa910bf9753a84a1bc33aeb3770e8fca521e032e \ + --hash=sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0 \ + --hash=sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8 \ + --hash=sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca \ + --hash=sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293 \ + --hash=sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1 \ + --hash=sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9 \ + --hash=sha256:e33e79a219105aa315439ee051bd50b6caa705dc4164a5aba6932c8ac3ce2d98 \ + --hash=sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8 \ + --hash=sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2 \ + --hash=sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e \ + --hash=sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b \ + --hash=sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75 \ + --hash=sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca \ + --hash=sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8 \ + --hash=sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03 \ + --hash=sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce \ + --hash=sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe \ + --hash=sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947 \ + --hash=sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb + # via pytest-cov +detect-secrets==1.5.0 \ + --hash=sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a \ + --hash=sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060 + # via python-basic-template (pyproject.toml) +distlib==0.4.0 \ + --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ + --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d + # via virtualenv +fastjsonschema==2.21.1 \ + --hash=sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4 \ + --hash=sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667 + # via nbformat +filelock==3.18.0 \ + --hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \ + --hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de + # via virtualenv +flask==2.3.3 \ + --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ + --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b + # via python-basic-template (pyproject.toml) +identify==2.6.12 \ + --hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \ + --hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6 + # via pre-commit +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via requests +iniconfig==2.1.0 \ + --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ + --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 + # via pytest +itsdangerous==2.2.0 \ + --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ + --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 + # via flask +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via flask +jsonschema==4.25.0 \ + --hash=sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716 \ + --hash=sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f + # via nbformat +jsonschema-specifications==2025.4.1 \ + --hash=sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af \ + --hash=sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608 + # via jsonschema +jupyter-core==5.8.1 \ + --hash=sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941 \ + --hash=sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0 + # via nbformat +markupsafe==3.0.2 \ + --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \ + --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ + --hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \ + --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ + --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ + --hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \ + --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ + --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \ + --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ + --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \ + --hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \ + --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \ + --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \ + --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ + --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ + --hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \ + --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ + --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ + --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ + --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \ + --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \ + --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ + --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \ + --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \ + --hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \ + --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \ + --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \ + --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ + --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ + --hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \ + --hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \ + --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ + --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \ + --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \ + --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ + --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \ + --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \ + --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ + --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ + --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \ + --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ + --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ + --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ + --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \ + --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ + --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ + --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ + --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ + --hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \ + --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ + --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \ + --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \ + --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ + --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ + --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ + --hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \ + --hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \ + --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ + --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ + --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \ + --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50 + # via + # jinja2 + # werkzeug +mypy==1.17.1 \ + --hash=sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341 \ + --hash=sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5 \ + --hash=sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849 \ + --hash=sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733 \ + --hash=sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81 \ + --hash=sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403 \ + --hash=sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6 \ + --hash=sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01 \ + --hash=sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91 \ + --hash=sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972 \ + --hash=sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8 \ + --hash=sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd \ + --hash=sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9 \ + --hash=sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0 \ + --hash=sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19 \ + --hash=sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb \ + --hash=sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd \ + --hash=sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99 \ + --hash=sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7 \ + --hash=sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056 \ + --hash=sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7 \ + --hash=sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a \ + --hash=sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed \ + --hash=sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94 \ + --hash=sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9 \ + --hash=sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58 \ + --hash=sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8 \ + --hash=sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5 \ + --hash=sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a \ + --hash=sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df \ + --hash=sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb \ + --hash=sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d \ + --hash=sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390 \ + --hash=sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b \ + --hash=sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b \ + --hash=sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14 \ + --hash=sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259 \ + --hash=sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b + # via python-basic-template (pyproject.toml) +mypy-extensions==1.1.0 \ + --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ + --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 + # via + # black + # mypy +nbformat==5.10.4 \ + --hash=sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a \ + --hash=sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b + # via nbstripout +nbstripout==0.8.1 \ + --hash=sha256:79a8c8da488d98c54c112fa87185045f0271a97d84f1d46918d6a3ee561b30e7 \ + --hash=sha256:eaac8b6b4e729e8dfe1e5df2c0f8ba44abc5a17a65448f0480141f80be230bb1 + # via python-basic-template (pyproject.toml) +nodeenv==1.9.1 \ + --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ + --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 + # via pre-commit +packaging==25.0 \ + --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ + --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f + # via + # black + # build + # pytest +pathspec==0.12.1 \ + --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ + --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # via + # black + # mypy +pip-tools==7.5.0 \ + --hash=sha256:30639f50961bb09f49d22f4389e8d7d990709677c094ce1114186b1f2e9b5821 \ + --hash=sha256:69758e4e5a65f160e315d74db46246fdbb30d549f1ed0c4236d057122c9b0f18 + # via python-basic-template (pyproject.toml) +platformdirs==4.3.8 \ + --hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \ + --hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4 + # via + # black + # jupyter-core + # virtualenv +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via + # pytest + # pytest-cov +pre-commit==4.2.0 \ + --hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \ + --hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd + # via python-basic-template (pyproject.toml) +pygments==2.19.2 \ + --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ + --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b + # via pytest +pyproject-hooks==1.2.0 \ + --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ + --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 + # via + # build + # pip-tools +pytest==8.4.1 \ + --hash=sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7 \ + --hash=sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c + # via + # pytest-beartype + # pytest-cov + # python-basic-template (pyproject.toml) +pytest-beartype==0.2.0 \ + --hash=sha256:9eeec379f812e0e5c9c1bb7e95d90a33a79c387034f2d735e03bc9aa0fd7cf3f \ + --hash=sha256:be6be04be4a143f965f21140d6661bab43dcd7c761d50cf6daa1b16848e5d2d0 + # via python-basic-template (pyproject.toml) +pytest-cov==6.2.1 \ + --hash=sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2 \ + --hash=sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5 + # via python-basic-template (pyproject.toml) +pyyaml==6.0.2 \ + --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ + --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ + --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ + --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ + --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 + # via + # detect-secrets + # pre-commit +referencing==0.36.2 \ + --hash=sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa \ + --hash=sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0 + # via + # jsonschema + # jsonschema-specifications +requests==2.32.4 \ + --hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \ + --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422 + # via + # detect-secrets + # python-basic-template (pyproject.toml) +rpds-py==0.26.0 \ + --hash=sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3 \ + --hash=sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b \ + --hash=sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5 \ + --hash=sha256:0c71c2f6bf36e61ee5c47b2b9b5d47e4d1baad6426bfed9eea3e858fc6ee8806 \ + --hash=sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e \ + --hash=sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4 \ + --hash=sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f \ + --hash=sha256:1533b7eb683fb5f38c1d68a3c78f5fdd8f1412fa6b9bf03b40f450785a0ab915 \ + --hash=sha256:1766b5724c3f779317d5321664a343c07773c8c5fd1532e4039e6cc7d1a815be \ + --hash=sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b \ + --hash=sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696 \ + --hash=sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323 \ + --hash=sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331 \ + --hash=sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8 \ + --hash=sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c \ + --hash=sha256:1d815d48b1804ed7867b539236b6dd62997850ca1c91cad187f2ddb1b7bbef19 \ + --hash=sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1 \ + --hash=sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8 \ + --hash=sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0 \ + --hash=sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318 \ + --hash=sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246 \ + --hash=sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256 \ + --hash=sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b \ + --hash=sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb \ + --hash=sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04 \ + --hash=sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1 \ + --hash=sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19 \ + --hash=sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7 \ + --hash=sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f \ + --hash=sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871 \ + --hash=sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84 \ + --hash=sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e \ + --hash=sha256:39bfea47c375f379d8e87ab4bb9eb2c836e4f2069f0f65731d85e55d74666387 \ + --hash=sha256:3ac51b65e8dc76cf4949419c54c5528adb24fc721df722fd452e5fbc236f5c40 \ + --hash=sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958 \ + --hash=sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44 \ + --hash=sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582 \ + --hash=sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7 \ + --hash=sha256:43f10b007033f359bc3fa9cd5e6c1e76723f056ffa9a6b5c117cc35720a80292 \ + --hash=sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a \ + --hash=sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba \ + --hash=sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953 \ + --hash=sha256:4b1f66eb81eab2e0ff5775a3a312e5e2e16bf758f7b06be82fb0d04078c7ac51 \ + --hash=sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9 \ + --hash=sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37 \ + --hash=sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9 \ + --hash=sha256:4f01a5d6444a3258b00dc07b6ea4733e26f8072b788bef750baa37b370266137 \ + --hash=sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3 \ + --hash=sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0 \ + --hash=sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8 \ + --hash=sha256:519067e29f67b5c90e64fb1a6b6e9d2ec0ba28705c51956637bac23a2f4ddae1 \ + --hash=sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e \ + --hash=sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618 \ + --hash=sha256:582462833ba7cee52e968b0341b85e392ae53d44c0f9af6a5927c80e539a8b67 \ + --hash=sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1 \ + --hash=sha256:59b2093224a18c6508d95cfdeba8db9cbfd6f3494e94793b58972933fcee4c6d \ + --hash=sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9 \ + --hash=sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da \ + --hash=sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0 \ + --hash=sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b \ + --hash=sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84 \ + --hash=sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d \ + --hash=sha256:69a607203441e07e9a8a529cff1d5b73f6a160f22db1097211e6212a68567d11 \ + --hash=sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1 \ + --hash=sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7 \ + --hash=sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79 \ + --hash=sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f \ + --hash=sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88 \ + --hash=sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0 \ + --hash=sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a \ + --hash=sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158 \ + --hash=sha256:7a48af25d9b3c15684059d0d1fc0bc30e8eee5ca521030e2bffddcab5be40226 \ + --hash=sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f \ + --hash=sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f \ + --hash=sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1 \ + --hash=sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad \ + --hash=sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7 \ + --hash=sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a \ + --hash=sha256:84cfbd4d4d2cdeb2be61a057a258d26b22877266dd905809e94172dff01a42ae \ + --hash=sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08 \ + --hash=sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03 \ + --hash=sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a \ + --hash=sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d \ + --hash=sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11 \ + --hash=sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6 \ + --hash=sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9 \ + --hash=sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb \ + --hash=sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca \ + --hash=sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf \ + --hash=sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9 \ + --hash=sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15 \ + --hash=sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19 \ + --hash=sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed \ + --hash=sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed \ + --hash=sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6 \ + --hash=sha256:a90a13408a7a856b87be8a9f008fff53c5080eea4e4180f6c2e546e4a972fb5d \ + --hash=sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387 \ + --hash=sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f \ + --hash=sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8 \ + --hash=sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5 \ + --hash=sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37 \ + --hash=sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45 \ + --hash=sha256:b5f7a446ddaf6ca0fad9a5535b56fbfc29998bf0e0b450d174bbec0d600e1d72 \ + --hash=sha256:b6d9e5a2ed9c4988c8f9b28b3bc0e3e5b1aaa10c28d210a594ff3a8c02742daf \ + --hash=sha256:b6e2c12160c72aeda9d1283e612f68804621f448145a210f1bf1d79151c47090 \ + --hash=sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20 \ + --hash=sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e \ + --hash=sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e \ + --hash=sha256:c5ab0ee51f560d179b057555b4f601b7df909ed31312d301b99f8b9fc6028284 \ + --hash=sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc \ + --hash=sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8 \ + --hash=sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867 \ + --hash=sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33 \ + --hash=sha256:cb28c1f569f8d33b2b5dcd05d0e6ef7005d8639c54c2f0be824f05aedf715255 \ + --hash=sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8 \ + --hash=sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c \ + --hash=sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323 \ + --hash=sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107 \ + --hash=sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d \ + --hash=sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a \ + --hash=sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2 \ + --hash=sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0 \ + --hash=sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41 \ + --hash=sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af \ + --hash=sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d \ + --hash=sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632 \ + --hash=sha256:e3730a48e5622e598293eee0762b09cff34dd3f271530f47b0894891281f051d \ + --hash=sha256:e5162afc9e0d1f9cae3b577d9c29ddbab3505ab39012cb794d94a005825bde21 \ + --hash=sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170 \ + --hash=sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c \ + --hash=sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf \ + --hash=sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd \ + --hash=sha256:eed5ac260dd545fbc20da5f4f15e7efe36a55e0e7cf706e4ec005b491a9546a0 \ + --hash=sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7 \ + --hash=sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3 \ + --hash=sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35 \ + --hash=sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674 \ + --hash=sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73 \ + --hash=sha256:fbaa70553ca116c77717f513e08815aec458e6b69a028d4028d403b3bc84ff37 \ + --hash=sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f \ + --hash=sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136 \ + --hash=sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83 \ + --hash=sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12 \ + --hash=sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9 + # via + # jsonschema + # referencing +ruff==0.12.7 \ + --hash=sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606 \ + --hash=sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71 \ + --hash=sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3 \ + --hash=sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860 \ + --hash=sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa \ + --hash=sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c \ + --hash=sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb \ + --hash=sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e \ + --hash=sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5 \ + --hash=sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd \ + --hash=sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303 \ + --hash=sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb \ + --hash=sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f \ + --hash=sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4 \ + --hash=sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423 \ + --hash=sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77 \ + --hash=sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69 \ + --hash=sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8 + # via python-basic-template (pyproject.toml) +traitlets==5.14.3 \ + --hash=sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7 \ + --hash=sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f + # via + # jupyter-core + # nbformat +typing-extensions==4.14.1 \ + --hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \ + --hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76 + # via + # mypy + # referencing +urllib3==2.5.0 \ + --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ + --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc + # via requests +virtualenv==20.33.0 \ + --hash=sha256:106b6baa8ab1b526d5a9b71165c85c456fbd49b16976c88e2bc9352ee3bc5d3f \ + --hash=sha256:47e0c0d2ef1801fce721708ccdf2a28b9403fa2307c3268aebd03225976f61d2 + # via pre-commit +werkzeug==3.1.3 \ + --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ + --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 + # via flask +wheel==0.45.1 \ + --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ + --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 + # via pip-tools + +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes and the requirement is not +# satisfied by a package already installed. Consider using the --allow-unsafe flag. +# pip +# setuptools diff --git a/requirements.in b/requirements.in deleted file mode 100644 index 749b7f9a..00000000 --- a/requirements.in +++ /dev/null @@ -1,2 +0,0 @@ -# Core project dependencies go here -# Development dependencies are in pyproject.toml [project.optional-dependencies.dev] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c71f0809..d1165559 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,204 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile requirements.in +# pip-compile --generate-hashes --output-file=requirements.txt pyproject.toml # +blinker==1.9.0 \ + --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ + --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc + # via flask +certifi==2025.8.3 \ + --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ + --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 + # via requests +charset-normalizer==3.4.2 \ + --hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \ + --hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \ + --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \ + --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \ + --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \ + --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \ + --hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \ + --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \ + --hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \ + --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \ + --hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \ + --hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \ + --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \ + --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \ + --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \ + --hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \ + --hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \ + --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \ + --hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \ + --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \ + --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \ + --hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \ + --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \ + --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \ + --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \ + --hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \ + --hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \ + --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \ + --hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \ + --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \ + --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \ + --hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \ + --hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \ + --hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \ + --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \ + --hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \ + --hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \ + --hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \ + --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \ + --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \ + --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \ + --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \ + --hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \ + --hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \ + --hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \ + --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \ + --hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \ + --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \ + --hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \ + --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \ + --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \ + --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \ + --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \ + --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \ + --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \ + --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \ + --hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \ + --hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \ + --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \ + --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \ + --hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \ + --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \ + --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \ + --hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \ + --hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \ + --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \ + --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \ + --hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \ + --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \ + --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \ + --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \ + --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \ + --hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \ + --hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \ + --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \ + --hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \ + --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \ + --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \ + --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \ + --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \ + --hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \ + --hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \ + --hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \ + --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \ + --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \ + --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \ + --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \ + --hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \ + --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \ + --hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \ + --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \ + --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f + # via requests +click==8.2.1 \ + --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ + --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b + # via flask +flask==2.3.3 \ + --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ + --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b + # via python-basic-template (pyproject.toml) +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via requests +itsdangerous==2.2.0 \ + --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ + --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 + # via flask +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via flask +markupsafe==3.0.2 \ + --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \ + --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ + --hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \ + --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ + --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ + --hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \ + --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ + --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \ + --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ + --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \ + --hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \ + --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \ + --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \ + --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ + --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ + --hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \ + --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ + --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ + --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ + --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \ + --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \ + --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ + --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \ + --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \ + --hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \ + --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \ + --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \ + --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ + --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ + --hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \ + --hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \ + --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ + --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \ + --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \ + --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ + --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \ + --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \ + --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ + --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ + --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \ + --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ + --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ + --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ + --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \ + --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ + --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ + --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ + --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ + --hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \ + --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ + --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \ + --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \ + --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ + --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ + --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ + --hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \ + --hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \ + --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ + --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ + --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \ + --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50 + # via + # jinja2 + # werkzeug +requests==2.32.4 \ + --hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \ + --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422 + # via python-basic-template (pyproject.toml) +urllib3==2.5.0 \ + --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ + --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc + # via requests +werkzeug==3.1.3 \ + --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ + --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 + # via flask diff --git a/scripts/dependency.sh b/scripts/dependency.sh index f9e288d4..91e9b47a 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -1,27 +1,53 @@ #!/usr/bin/env bash -set -e +set -euo pipefail -# Dependency management script for hybrid setup: -# - Production dependencies: requirements.in -> requirements.txt (via pip-tools) -# - Development dependencies: pyproject.toml [project.optional-dependencies.dev] - -# Config -VENV_DIR="venv" +# Hybrid dependency management script: +# Usage: +# ./dependency.sh # install both prod & dev +# ./dependency.sh --prod # install only production deps +# ./dependency.sh --dev # install only development deps # Colors YELLOW='\033[1;33m' GREEN='\033[1;32m' RESET='\033[0m' -echo -e "${YELLOW}๐Ÿ“ฆ Activating virtualenv...${RESET}" -if [ -f "$VENV_DIR/bin/activate" ]; then - source "$VENV_DIR/bin/activate" +# Parse args +INSTALL_PROD=false +INSTALL_DEV=false + +if [ $# -eq 0 ]; then + INSTALL_PROD=true + INSTALL_DEV=true +else + for arg in "$@"; do + case $arg in + --prod) + INSTALL_PROD=true + ;; + --dev) + INSTALL_DEV=true + ;; + *) + echo "โŒ Unknown option: $arg" + echo "Usage: $0 [--prod] [--dev]" + exit 1 + ;; + esac + done +fi + +echo -e "${YELLOW}โณ Activating virtualenv at '${VENV_DIR}'...${RESET}" +if [ -f "${VENV_DIR}/bin/activate" ]; then + # shellcheck disable=SC1090 + source "${VENV_DIR}/bin/activate" else - echo "โŒ No virtualenv found at $VENV_DIR. Run: python -m venv $VENV_DIR" + echo "โŒ Virtualenv not found at '${VENV_DIR}'." + echo " Create one with: python -m venv ${VENV_DIR}" exit 1 fi -echo -e "${YELLOW}๐Ÿ” Checking pip-tools...${RESET}" +echo -e "${YELLOW}๐Ÿ” Ensuring pip-tools is installed...${RESET}" if ! pip show pip-tools &>/dev/null; then echo -e "${YELLOW}โžก๏ธ Installing pip-tools...${RESET}" pip install pip-tools @@ -29,17 +55,30 @@ else echo -e "${GREEN}โœ… pip-tools is already installed.${RESET}" fi -echo -e "${YELLOW}๐Ÿงฎ Compiling requirements.txt from requirements.in...${RESET}" -pip-compile requirements.in +if [ "$INSTALL_PROD" = true ]; then + echo -e "${YELLOW}๐Ÿงฎ Compiling production lockfile from '${PYPROJECT}'...${RESET}" + pip-compile "${PYPROJECT}" \ + --output-file="${PROD_LOCK}" \ + --generate-hashes + + echo -e "${YELLOW}๐Ÿ“ฅ Installing production dependencies...${RESET}" + pip install --require-hashes -r "${PROD_LOCK}" +fi -echo -e "${YELLOW}๐Ÿ“ฅ Installing production dependencies...${RESET}" -pip install -r requirements.txt +if [ "$INSTALL_DEV" = true ]; then + echo -e "${YELLOW}๐Ÿ›  Compiling development lockfile from '${PYPROJECT}' extras...${RESET}" + pip-compile "${PYPROJECT}" \ + --extra=dev \ + --output-file="${DEV_LOCK}" \ + --generate-hashes -echo -e "${YELLOW}๏ฟฝ๏ธ Installing development dependencies...${RESET}" -pip install -e .[dev] + echo -e "${YELLOW}๐Ÿ“ฅ Installing development dependencies...${RESET}" + pip install --require-hashes -r "${DEV_LOCK}" +fi -echo -e "${YELLOW}๏ฟฝ๐Ÿ” Verifying environment with pip check...${RESET}" +echo -e "${YELLOW}๐Ÿ”Ž Verifying installed packages...${RESET}" pip check -echo -e "${GREEN}โœ… Setup complete! All dependencies are installed and compatible.${RESET}" -echo -e "${GREEN}๐Ÿ“‹ Installed: Production deps (requirements.txt) + Development deps (pyproject.toml)${RESET}" +echo -e "${GREEN}๐ŸŽ‰ Setup complete!${RESET}" +[ "$INSTALL_PROD" = true ] && echo -e "${GREEN} โ€ข Production deps from ${PROD_LOCK}${RESET}" +[ "$INSTALL_DEV" = true ] && echo -e "${GREEN} โ€ข Dev deps from ${DEV_LOCK}${RESET}" \ No newline at end of file diff --git a/scripts/setup.sh b/scripts/setup.sh index b86c61bc..9f170421 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,5 +1,5 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +set -euo pipefail # Complete development environment setup script for Python Basic Template # This script sets up everything a developer needs to start working on the project: @@ -17,98 +17,85 @@ RED='\033[0;31m' BLUE='\033[0;34m' RESET='\033[0m' -step() { - echo -e "${CYAN}๐Ÿ‘‰ $1${RESET}" -} - -success() { - echo -e "${GREEN}โœ… $1${RESET}" -} - -warn() { - echo -e "${YELLOW}โš ๏ธ $1${RESET}" -} - -error() { - echo -e "${RED}โŒ $1${RESET}" - exit 1 -} - -info() { - echo -e "${BLUE}โ„น๏ธ $1${RESET}" -} - -section() { +step() { echo -e "${CYAN}๐Ÿ‘‰ $1${RESET}"; } +success(){ echo -e "${GREEN}โœ… $1${RESET}"; } +warn() { echo -e "${YELLOW}โš ๏ธ $1${RESET}"; } +error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } +info() { echo -e "${BLUE}โ„น๏ธ $1${RESET}"; } +section(){ echo -e "\n${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" echo -e "${BLUE}๐Ÿ”ง $1${RESET}" echo -e "${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" } -# ------------------------------------------------------ -# ๏ฟฝ SECTION: Complete Development Environment Setup -# ------------------------------------------------------ - setup_development_environment() { section "SYSTEM REQUIREMENTS CHECK" - step "Verifying required system packages..." - info "Checking for Python 3..." - command -v python3 >/dev/null || error "Python 3 is not installed. Please install Python 3.12 or later." + command -v python3 >/dev/null || error "Python 3 is not installed. Please install Python 3.12+." PYTHON_VERSION=$(python3 --version | cut -d' ' -f2) success "Python $PYTHON_VERSION found." - + info "Checking for pip..." command -v pip >/dev/null || error "pip is not installed. Please install pip." PIP_VERSION=$(pip --version | cut -d' ' -f2) success "pip $PIP_VERSION found." - + info "Checking for curl..." command -v curl >/dev/null || error "curl is not installed. Please install curl." success "curl is available." - + info "Checking for git..." command -v git >/dev/null || error "git is not installed. Please install git." GIT_VERSION=$(git --version | cut -d' ' -f3) success "git $GIT_VERSION found." section "VIRTUAL ENVIRONMENT SETUP" - step "Creating Python virtual environment..." - if [ -d "venv" ]; then - warn "Virtual environment already exists. Removing old one..." - rm -rf venv + if [ -d "$VENV_DIR" ]; then + warn "Virtual environment already exists." + echo -e "${YELLOW}Choose an option:${RESET}" + echo " 1) Skip creating a new virtual environment (keep existing)" + echo " 2) Remove old one and create a new virtual environment" + read -p "Enter 1 or 2 [default: 2]: " VENV_CHOICE + VENV_CHOICE=${VENV_CHOICE:-2} + if [ "$VENV_CHOICE" = "1" ]; then + success "Keeping existing virtual environment." + source $VENV_DIR/bin/activate + elif [ "$VENV_CHOICE" = "2" ]; then + warn "Removing old virtual environment..." + rm -rf $VENV_DIR + python3 -m venv $VENV_DIR + source $VENV_DIR/bin/activate + success "Virtual environment created and activated." + else + error "Invalid choice. Exiting." + fi + else + python3 -m venv $VENV_DIR + source $VENV_DIR/bin/activate + success "Virtual environment created and activated." fi - - python3 -m venv venv - source venv/bin/activate - success "Virtual environment created and activated." - + step "Upgrading pip in virtual environment..." pip install --upgrade pip PIP_NEW_VERSION=$(pip --version | cut -d' ' -f2) success "pip upgraded to version $PIP_NEW_VERSION." section "HYBRID DEPENDENCY MANAGEMENT" - info "This project uses a hybrid dependency approach:" - info "โ€ข Production dependencies: requirements.in โ†’ requirements.txt (via pip-tools)" - info "โ€ข Development dependencies: pyproject.toml [project.optional-dependencies.dev]" - - step "Installing dependency management tools..." + info " โ€ข Production deps : declared in $PYPROJECT โ†’ locked to $PROD_LOCK" + info " โ€ข Development deps : declared in $PYPROJECT [project.optional-dependencies.dev] โ†’ locked to $DEV_LOCK" + + step "Installing dependency management tools (pip-tools, build, wheel)..." pip install pip-tools build wheel success "pip-tools, build, and wheel installed." - - step "Running dependency setup script..." + + step "Running dependency setup script (compiles & installs deps)..." ./scripts/dependency.sh - success "All dependencies installed successfully." - - step "Verifying dependency installation..." - pip list --format=columns - success "Dependency verification complete." + success "Production and development dependencies installed." section "SYSTEM LOCALE CONFIGURATION" - step "Updating system locales..." sudo apt update sudo apt install -y locales @@ -117,11 +104,9 @@ setup_development_environment() { success "System locales configured." section "DIRENV SETUP" - step "Installing direnv for environment variable management..." sudo apt install -y direnv success "direnv installed." - step "Configuring direnv shell integration..." PROFILE="${HOME}/.bashrc" if ! grep -q 'direnv hook bash' "$PROFILE"; then @@ -130,11 +115,7 @@ setup_development_environment() { else warn "direnv hook already present in $PROFILE." fi - - step "Reloading shell configuration..." - source "$PROFILE" success "Shell configuration reloaded." - step "Enabling direnv for this project..." if [ -f ".envrc" ]; then direnv allow @@ -144,136 +125,83 @@ setup_development_environment() { fi section "PRE-COMMIT HOOKS SETUP" - step "Installing pre-commit hooks..." if [[ -f ".pre-commit-config.yaml" ]]; then - source venv/bin/activate pre-commit install success "Pre-commit hooks installed." - step "Updating pre-commit hook repositories..." pre-commit autoupdate - success "Pre-commit hooks updated to latest versions." - + success "Pre-commit hooks updated." step "Running pre-commit on all files (initial check)..." - pre-commit run --all-files || warn "Some pre-commit checks failed. This is normal for initial setup." - info "Pre-commit will now run automatically on git commits." + pre-commit run --all-files || warn "Some pre-commit checks failed (initial run)." + info "Pre-commit will now run automatically on commits." else - warn ".pre-commit-config.yaml not found. Skipping hook installation." + warn ".pre-commit-config.yaml not found. Skipping hooks install." fi section "SECRET MANAGEMENT SETUP" - step "Checking 1Password CLI integration..." if op --version &>/dev/null; then - success "1Password CLI is installed and accessible." - + success "1Password CLI is installed." step "Testing 1Password connection..." if op account list &>/dev/null; then - success "1Password CLI is connected to your account." + success "1Password CLI is connected." else - warn "1Password CLI is installed but not connected to your account." - info "You may need to sign in: op signin" + warn "1Password CLI not signed in." fi else - warn "1Password CLI not found." - info "1Password CLI must be installed on Windows with CLI integration enabled and linked to WSL." - info "Follow the instructions here:" - info "https://www.notion.so/techletes/Python-Basic-Template-Repository-24117d03144d809f92f9dc682d17d347" - info "Re-run this setup script after installing 1Password CLI." + error "1Password CLI not found. Install & configure before secrets workflow." + exit 1 fi - - step "Setting up secrets baseline with detect-secrets..." + step "Initializing secrets baseline with detect-secrets..." if [ -f ".secrets.baseline" ]; then - warn "Existing .secrets.baseline found. Backing up..." - cp .secrets.baseline .secrets.baseline.backup + warn "Existing .secrets.baseline found; backing up." + cp .secrets.baseline .secrets.baseline.bak fi - detect-secrets scan > .secrets.baseline - success "Secrets baseline created in .secrets.baseline." - - step "Verifying secrets configuration..." + success "Secrets baseline created." detect-secrets audit .secrets.baseline success "Secrets configuration verified." section "DEVELOPMENT TOOLS VERIFICATION" - - step "Verifying installed development tools..." - - info "Checking Black (code formatter)..." - black --version - success "Black is working." - - info "Checking isort (import sorter)..." - isort --version - success "isort is working." - - info "Checking flake8 (linter)..." - flake8 --version - success "flake8 is working." - - info "Checking mypy (type checker)..." - mypy --version - success "mypy is working." - - info "Checking pytest (test runner)..." - pytest --version - success "pytest is working." + step "Verifying development tools..." + for tool in black ruff mypy pytest beartype; do + info "Checking $tool..." + "pip show $tool" &>/dev/null && success "$tool is installed." || warn "$tool missing." + done section "PROJECT VALIDATION" - step "Running final project validation..." - - info "Checking Python imports..." - python -c "import utils.secrets; import utils.utils; print('โœ… All project modules import successfully')" - - info "Running syntax check on all Python files..." - find . -name "*.py" -not -path "./venv/*" -exec python -m py_compile {} \; - success "All Python files have valid syntax." - - if [ -f "requirements.txt" ] && [ -s "requirements.txt" ]; then + info "Checking imports..." + python - <<'PYCODE' +import utils.secrets, utils.utils +print("โœ… All project modules import successfully") +PYCODE + info "Syntax-checking all Python files..." + find . -name "*.py" -not -path "./$VENV_DIR/*" -exec python -m py_compile {} \; + success "Python syntax valid." + if [ -s "$PROD_LOCK" ]; then info "Verifying production dependencies..." pip check - success "All dependencies are compatible." + success "Dependencies compatible." else - info "No production dependencies to verify (requirements.txt is empty)." + info "No prod dependencies to verify." fi section "SETUP COMPLETE" - - echo -e "\n${GREEN}๐ŸŽ‰ DEVELOPMENT ENVIRONMENT SETUP COMPLETE! ๐ŸŽ‰${RESET}\n" - - echo -e "${BLUE}๐Ÿ“‹ What was installed:${RESET}" - echo -e " โ€ข Python virtual environment (venv/)" - echo -e " โ€ข Production dependencies (from requirements.txt)" - echo -e " โ€ข Development tools (from pyproject.toml)" - echo -e " โ€ข Pre-commit hooks for code quality" - echo -e " โ€ข Secret management with detect-secrets" - echo -e " โ€ข direnv for environment variables" - - echo -e "\n${BLUE}๐Ÿš€ Next steps:${RESET}" - echo -e " 1. Restart your terminal or run: source ~/.bashrc" - echo -e " 2. Test the setup: python example/using_secrets.py" - echo -e " 3. Start coding! The environment is ready." - - echo -e "\n${BLUE}๐Ÿ’ก Useful commands:${RESET}" - echo -e " โ€ข Activate environment: source venv/bin/activate" - echo -e " โ€ข Install new production dep: Add to requirements.in, run ./scripts/dependency.sh" - echo -e " โ€ข Install new dev tool: Add to pyproject.toml, run pip install -e .[dev]" - echo -e " โ€ข Run tests: pytest" - echo -e " โ€ข Format code: black . && isort ." - echo -e " โ€ข Type check: mypy ." - - if ! op --version &>/dev/null || ! op account list &>/dev/null; then - echo -e "\n${YELLOW}โš ๏ธ 1Password CLI setup incomplete:${RESET}" - echo -e " Install and configure 1Password CLI to use secret management features." - fi - - echo -e "\n${GREEN}Happy coding! ๐Ÿโœจ${RESET}" + echo -e "\n${GREEN}๐ŸŽ‰ DEVELOPMENT ENVIRONMENT SETUP COMPLETE!${RESET}\n" + echo -e "${BLUE}Installed:${RESET}" + echo " โ€ข $VENV_DIR/" + echo " โ€ข Production deps ($PROD_LOCK)" + echo " โ€ข Dev tools ($DEV_LOCK)" + echo " โ€ข Pre-commit hooks" + echo " โ€ข Secret baseline (.secrets.baseline)" + echo " โ€ข direnv" + echo -e "\n${BLUE}Next steps:${RESET}" + echo " 1. Restart terminal or run: source ~/.bashrc" + echo " 2. Test with: python example/using_secrets.py" + echo " 3. Start coding!" } -# ------------------------------------------------------ -# ๐Ÿ Main Execution -# ------------------------------------------------------ - -setup_development_environment \ No newline at end of file +# Main +setup_development_environment diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..9084afae --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +# setup.py is meant for compatibility with older Python packaging tools. +# It is not strictly necessary if you are using PEP 517/518 compliant build tools. +# However, it can be useful for certain environments or legacy systems. +# Do not remove or modify this file unless you are sure you do not need it. +from setuptools import setup + +if __name__ == "__main__": + setup() From 60cf957d7d22374323a125cf154e426b57fbe7b1 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 14:08:53 +0200 Subject: [PATCH 019/199] Remove outdated comments from pyproject.toml and enhance check-requirements.sh to validate requirements-dev.txt modifications --- pyproject.toml | 3 --- scripts/hooks/check-requirements.sh | 8 ++++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 75b43c30..11adbbb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,3 @@ -# This file is only used for tool configuration. -# Dependency locking is handled via pip-tools using requirements.in / requirements.txt. - [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" diff --git a/scripts/hooks/check-requirements.sh b/scripts/hooks/check-requirements.sh index 44a144d2..da693608 100755 --- a/scripts/hooks/check-requirements.sh +++ b/scripts/hooks/check-requirements.sh @@ -5,4 +5,12 @@ if git diff --cached --name-only | grep -q "^requirements.txt$"; then echo "Use 'pip-compile' instead. See 2.2 in the docs." exit 1 fi +fi + +if git diff --cached --name-only | grep -q "^requirements-dev.txt$"; then + if ! grep -q "# This file is autogenerated by pip-compile" requirements-dev.txt; then + echo "โŒ ERROR: requirements-dev.txt appears to have been modified manually." + echo "Use 'pip-compile' instead. See 2.2 in the docs." + exit 1 + fi fi \ No newline at end of file From ad5a33957dc0bd6f9cec44c6d95d1c76f176a3f0 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 14:10:12 +0200 Subject: [PATCH 020/199] Update docs --- docs/0_setup.md | 380 ++++++++++++++++++++++++++++++++ docs/1_secret_management.md | 62 +----- docs/2_dependency_management.md | 168 +++++--------- 3 files changed, 448 insertions(+), 162 deletions(-) create mode 100644 docs/0_setup.md diff --git a/docs/0_setup.md b/docs/0_setup.md new file mode 100644 index 00000000..e95df161 --- /dev/null +++ b/docs/0_setup.md @@ -0,0 +1,380 @@ +# 0. Project setup + +Setting up a new Python project requires several important steps to ensure a secure and efficient development workflowโ€”especially when working with secret management, virtual environments, and Git hygiene. + +--- + +## **0.1 Prerequisite steps** + +### 0.1.1 Set-up 1Password (CLI) + +1. **Download and activate 1Password Desktop** + 1. Download and install from here: + + [Download 1Password for Windows | 1Password](https://1password.com/downloads/windows) + + 2. Log in with your account. + 3. Go to โ€˜Settingsโ€™ > โ€˜Securityโ€™ > Enable โ€˜Unlock with Windows Helloโ€™ + 4. Go to โ€˜settingsโ€™ > โ€˜Developerโ€™ > Enable โ€˜Integrate with 1Password CLIโ€™. + +2. **Set-up 1password cli** + 1. Open windows powershell and run + + ```bash + winget install 1password-cli + ``` + + 2. In your file explorer navigate to: + + `C:/Users//AppData/Local/Microsoft/WinGet/Packages/<1password_cli_package_id>/op.exe` and copy the absolute path that leads to the `op.exe` + + 3. You then modify the path by replacing `C:`/ with `/mnt/c/` . Also replace all backward slashes with forward slashes. Your final path should look something like: + + `/mnt/c/Users/thome/AppData/Local/Microsoft/WinGet/Packages/AgileBits.1Password.CLI_Microsoft.Winget.Source_8wekyb3d8bbwe/op.exe` + + 4. Then in WSL run the following command: + + ```bash + ln -s /usr/bin/op + ``` + + 5. Run in WSL: + + ```bash + op --version + ``` + + If you see a version number, then the link was succesfull and you can access 1Password in WSL + + +--- + +## 0.2 Automatic set-up script + +To simplify the setup process, all mandatory setup steps have been implemented into a single, automated script. After cloning or forking this repository, you can configure your entire development environment by simply running: + +```bash +bash scripts/setup.sh +``` + +This script will take care of everything needed to get started, so you can focus on writing code right away. + +If you'd prefer to walk through the setup manually or want to understand what's happening under the hood, a detailed breakdown of each step is provided in the sections below. + +## 0.3 Manual set-up development environment + +Follow these steps to prepare your local machine for development with the Python Basic Template. Perform each section in order. + +--- + +### 0.3.1 System Requirements Check + +1. **Verify Python 3** + + ```bash + python3 --version + ``` + + - Ensure the version is **3.12** or later. +2. **Verify pip** + + ```bash + pip --version + ``` + +3. **Verify curl** + + ```bash + command -v curl + ``` + +4. **Verify git** + + ```bash + git --version + ``` + + +If any of these commands fail, install the missing package before proceeding. + +--- + +### 0.3.2 Virtual Environment Setup + +1. **Remove existing `venv`** (if present) + + ```bash + rm -rf venv + ``` + +2. **Create a new virtual environment** + + ```bash + python3 -m venv venv + ``` + +3. **Activate the virtual environment** + + ```bash + source venv/bin/activate + ``` + +4. **Upgrade pip** + + ```bash + pip install --upgrade pip + ``` + + +--- + +### 0.3.3 Hybrid Dependency Management + +This project uses a hybrid approach to dependency management: + +- **Production dependencies**: declared in `pyproject.toml`, locked to `requirements.txt`. +- **Development dependencies**: listed under `project.optional-dependencies.dev` in `pyproject.toml`, locked to `dev-requirements.txt`. + +You can use the helper script or perform each step manually: + +**0.3.3.1 Usage of `dependency.sh`** + +- Install both production and development dependencies: + + ``` + ./scripts/dependency.sh + ``` + +- Install only production dependencies: + + ``` + ./scripts/dependency.sh --prod + ``` + +- Install only development dependencies: + + ``` + ./scripts/dependency.sh --dev + ``` + + +**0.3.3.2 Manual Steps** + +If you prefer to run each step manually, follow these commands: + +1. **Activate the virtual environment** (must be in project root): + + ``` + source venv/bin/activate + ``` + +2. **Ensure `pip-tools` is installed**: + + ``` + pip show pip-tools &>/dev/null || pip install pip-tools + ``` + +3. **Compile production lockfile**: + + ``` + pip-compile pyproject.toml \ + --output-file=requirements.txt \ + --generate-hashes + ``` + +4. **Install production dependencies**: + + ``` + pip install --require-hashes -r requirements.txt + ``` + +5. **Compile development lockfile**: + + ``` + pip-compile pyproject.toml \ + --extra=dev \ + --output-file=requirements-dev.txt \ + --generate-hashes + ``` + +6. **Install development dependencies**: + + ``` + pip install --require-hashes -r requirements-dev.txt + ``` + +7. **Verify that all packages are compatible**: + + ``` + pip check + ``` + + +--- + +### 0.3.4 System Locale Configuration + +1. **Update package lists and install locales** + + ```bash + sudo apt update + sudo apt install -y locales + ``` + +2. **Generate and apply the `en_US.UTF-8` locale** + + ```bash + sudo locale-gen en_US.UTF-8 + sudo update-locale LANG=en_US.UTF-8 + ``` + + +--- + +### 0.3.5 direnv Setup + +1. **Install `direnv`** + + ```bash + sudo apt install -y direnv + ``` + +2. **Add the `direnv` hook to your shell profile** + + ```bash + echo 'eval "$(direnv hook bash)"' >> ~/.bashrc + source ~/.bashrc + ``` + +3. **Allow the project `.envrc`** + + ```bash + cd path/to/project + direnv allow + ``` + + +--- + +### 0.3.6 Pre-commit Hooks Setup + +1. **Install Git hooks** + + ```bash + pre-commit install + ``` + +2. **Update hook definitions** + + ```bash + pre-commit autoupdate + + ``` + +3. **Run all checks once** + + ```bash + pre-commit run --all-files + + ``` + + +--- + +### 0.3.7. Secret Management Setup + +1. **Check for 1Password CLI** + + ```bash + op --version + ``` + +2. **Verify your 1Password account** + + ```bash + op account list + ``` + +3. **Initialize a baseline for `detect-secrets`** + + ```bash + mv .secrets.baseline .secrets.baseline.bak 2>/dev/null || true + detect-secrets scan > .secrets.baseline + detect-secrets audit .secrets.baseline + ``` + + +--- + +### 0.3.8 Development Tools Verification + +1. **Black** + + ```bash + pip show black + ``` + +2. **Ruff** + + ```bash + pip show ruff + ``` + +3. **Mypy** + + ```bash + pip show mypy + ``` + +4. **Beartype** + + ```bash + pip show beartype + ``` + +5. **pytest** + + ```bash + pip show pytest + + ``` + + +--- + +### 0.3.9 Project Validation + +1. **Ensure all modules import correctly** + + ```bash + python3 - << 'EOF' + import utils.secrets + import utils.utils + print("Imports successful") + EOF + + ``` + +2. **Syntax check all Python files** + + ```bash + find . -name "*.py" -not -path "./venv/*" \ + -exec python3 -m py_compile {} + + ``` + +3. **Verify installed dependencies** + + ```bash + pip check + ``` + + +--- + +### 0.3.10 Next Steps + +- Restart your terminal or run `source ~/.bashrc` +- Start development: run your application or tests locally +- Refer to the project README for usage and contribution guidelines + +***End of manual setup instructions.*** \ No newline at end of file diff --git a/docs/1_secret_management.md b/docs/1_secret_management.md index d4ede5fe..810c8f64 100644 --- a/docs/1_secret_management.md +++ b/docs/1_secret_management.md @@ -1,55 +1,8 @@ -# Secret management +# 1. Secret management We use **1Password CLI** (`op`) and **GitHub Secrets** to fetch secrets securelyโ€”**no plaintext `.env` files** are ever stored in Git. -Perform all of the following in your development environment on WSL. - -### 1.2.1 Prerequisites (from scratch) - -1. Verify 1Password cli is set up correctly - - ```bash - op --version - ``` - - This should show the version number. If not check the steps from 1.1.1 - -2. **Create virtual environment** - - ```bash - python3 -m venv venv - source venv/bin/activate - ``` - -3. **Install Dev Dependencies** - - ```bash - pip install -r requirements-dev.txt - ``` - -4. **Install Direnv** and allow - - ```bash - sudo apt install direnv - direnv allow - ``` - -5. Then add this to your shell profile (`~/.bashrc` ) - - ```bash - eval "$(direnv hook bash)" - ``` - -6. Then reload your shell: - - ```bash - source ~/.bashrc - ``` - - ---- - -### 1.2.2 Local Development +## 1.1 Local Development 1. **Authentication for 1Password is handled automatically** @@ -101,7 +54,7 @@ On each commit, secrets are blocked and notebook outputs cleared: --- -### 1.2.3 CI / GitHub Actions +## 1.2 CI / GitHub Actions For CI, we **do not** use 1Password directly. Instead, define your secrets in **GitHub Settings > Secrets**: @@ -124,7 +77,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.x' + python-version: '3.12' - name: Install dependencies run: pip install -r requirements.txt @@ -141,8 +94,11 @@ This way, CI runners receive secrets securely from GitHub and never see 1Passwor --- -### 1.2.4 Rotating & Auditing +## 1.3 Rotating & Auditing -- **Rotate** immediately in 1Password or GitHub when a secret is compromised. +- **Rotate** immediately in 1Password or GitHub when a secret is compromised. Follow this action plan: + + [Compromised secret action plan](https://www.notion.so/Compromised-secret-action-plan-24517d03144d805eba09e65cd5fe6a49?pvs=21) + - **Audit** with `detect-secrets scan > .secrets.baseline` after updates. - Maintain a clear **playbook** in `SECURITY.md` for incident response. \ No newline at end of file diff --git a/docs/2_dependency_management.md b/docs/2_dependency_management.md index a7b33062..54df0078 100644 --- a/docs/2_dependency_management.md +++ b/docs/2_dependency_management.md @@ -1,148 +1,98 @@ # 2. Dependency management -This project uses a modern Python dependency management approach combining [`pip-tools`](https://etc.com/jazzband/pip-tools) for production dependencies and `pyproject.toml` for development tools. - -Dependencies are separated into two categories: -- **Production dependencies**: Managed via `requirements.in` โ†’ `requirements.txt` -- **Development dependencies**: Managed via `pyproject.toml` +This project uses a modern Python dependency management approach leveraging **pip-tools** with a single source of truth in **`pyproject.toml`**, while still generating fully pinned lockfiles for reproducible installs. --- -## 2.1 Overview of the dependency workflow +## 2.1 Workflow Overview -The project separates different types of dependencies: +- **Production dependencies** are declared under `dependencies` in `pyproject.toml` and locked to `requirements.txt` via pip-tools. +- **Development dependencies** are declared under `[project.optional-dependencies].dev` in `pyproject.toml` and locked to `requirements-dev.txt` via pip-tools. -- **`requirements.in`**: Production dependencies that your project needs to run (e.g. `requests`, `pandas`, `fastapi`). -- **`requirements.txt`**: Generated by `pip-tools`, contains all resolved production packages with pinned versions. -- **`pyproject.toml`**: Development tools and testing dependencies (e.g. `pre-commit`, `pytest`, `black`, `mypy`). +This setup ensures: -This approach provides: -- Clean separation between production and development dependencies -- Reproducible builds with pinned versions -- Optional installation of development tools -- Consistency across environments +- Clear separation of runtime vs. tooling packages +- Fully pinned, reproducible installs using lockfiles +- One declarative manifest (`pyproject.toml`) for all dependencies --- -## 2.2 Installation Workflow +## 2.2 Using the helper script (`./scripts/dependency.sh`) -To add dependencies, follow these steps based on the type: +The provided script automates compilation and installation of both production and development dependencies. -### For Production Dependencies +```bash +# Install both production and development dependencies +./scripts/dependency.sh -1. **Add the dependency to `requirements.in`** - - For example, to add `requests`: +# Install only production dependencies: +./scripts/dependency.sh --prod + +# Install only development dependencies: +./scripts/dependency.sh --dev +``` + +### 2.2.1 Under the hood (manual steps) + +1. Activates the project virtual environment (`venv/bin/activate`). +2. Installs or verifies **pip-tools**. +3. Compiles the production lockfile from `pyproject.toml`: + ```bash + pip-compile pyproject.toml --output-file=requirements.txt --generate-hashes ``` - # Core project dependencies go here - # Development dependencies are in pyproject.toml [project.optional-dependencies.dev] - requests - pandas - ``` - - - -2. **Run the dependency script** - - Use the project's dependency script to resolve and install: +4. Installs production packages: ```bash - ./scripts/dependency.sh + pip install --require-hashes -r requirements.txt ``` - **This will:** - - - Activate your virtual environment (from `.venv/`) - - Install `pip-tools` if needed - - Compile `requirements.txt` from `requirements.in` - - Install all dependencies - - Run `pip check` to ensure compatibility - -3. **Commit the changes** - - Always commit both files: +5. Compiles the development lockfile from the `dev` extras: ```bash - git add requirements.in requirements.txt - git commit -m "Add requests dependency" + pip-compile pyproject.toml --extra=dev --output-file=requirements-dev.txt --generate-hashes ``` - -### For Development Dependencies - -To add development tools (linters, formatters, testing tools): - -1. **Add to `pyproject.toml`** - Edit the `[project.optional-dependencies.dev]` section: - - ```toml - [project.optional-dependencies] - dev = [ - "pre-commit", - "detect-secrets", - "nbstripout", - "pytest", - "pytest-cov", - "black", - "isort", - "flake8", - "mypy", - "pip-tools", - "your-new-tool", # Add here - ] - ``` - -2. **Install the updated dependencies** +6. Installs development packages: ```bash - pip install -e .[dev] + pip install --require-hashes -r requirements-dev.txt ``` - -### Installation Options - -- **Minimal (production only)**: `pip install -e .` -- **Full development setup**: `pip install -e .[dev]` -- **Using the script**: `./scripts/dependency.sh` (installs both) ---- - -## 2.4 Sanity Check - -To manually verify that all installed dependencies are compatible, run: - -```bash -pip check -``` - -This is automatically done by the setup script as well. + +7. Runs `pip check` to validate compatibility. --- -## 2.5 Summary of Rules +## 2.3 Best Practices and Rules | โœ… Do This | โŒ Don't Do This | | --- | --- | -| Add production deps to `requirements.in` | Don't run `pip install ` manually | -| Add dev tools to `pyproject.toml` | Don't edit `requirements.txt` by hand | -| Use `./scripts/dependency.sh` or `pip install -e .[dev]` | Don't mix production and dev dependencies | -| Commit both `requirements.in` and `requirements.txt` | Don't skip `pip check` or forget to compile | -| Use `pip install -e .` for minimal installs | Don't install dev tools in production | +| Declare all deps in `pyproject.toml` | Scatter deps across multiple files | +| Use `./scripts/dependency.sh` or manual pip-compile commands | Run `pip install ` manually | +| Commit both `requirements.txt` and `requirements-dev.txt` lockfiles | Edit lockfiles by hand | +| Pin production installs via `requirements.txt` | Mix runtime and dev dependencies | +| Pin dev installs via `requirements-dev.txt` | Forget to regenerate lockfiles | +| Run `pip check` after installation | Skip integrity verification | --- -## 2.6 Migration from Previous Setup - -If you're updating from an older version of this template where all dependencies were in `requirements.in`: +## 2.4 Migration from Older Setups -1. **Identify development tools** in your current `requirements.in` -2. **Move them to `pyproject.toml`** in the `[project.optional-dependencies.dev]` section -3. **Keep only production dependencies** in `requirements.in` -4. **Regenerate `requirements.txt`** with `pip-compile requirements.in` -5. **Install dev dependencies** with `pip install -e .[dev]` +If migrating from a legacy `requirements.in` workflow: -Development tools typically include: `pre-commit`, `detect-secrets`, `nbstripout`, `pytest`, `black`, `isort`, `flake8`, `mypy`, `bandit`, \ No newline at end of file +1. **Move all runtime dependencies** into `[project.dependencies]` in `pyproject.toml`. +2. **Move all tooling dependencies** into `[project.optional-dependencies.dev]`. +3. **Remove** the old `requirements.in` file. +4. **Regenerate** lockfiles: + + ```bash + pip-compile pyproject.toml --output-file=requirements.txt --generate-hashes + pip-compile pyproject.toml --extra=dev --output-file=requirements-dev.txt --generate-hashes + ``` + +5. **Install** with: + + ```bash + ./scripts/dependency.sh + ``` \ No newline at end of file From 8106b6c2bf9f6a61a8486186b51e3ac49301898c Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 14:34:06 +0200 Subject: [PATCH 021/199] Update docs --- CODE_OF_CONDUCT.md | 77 ++++++++++++++++++++ CONTRIBUTING.md | 160 ++++++++++++++++++++++++++++++++++++++++++ README.md | 82 ++++++++++++++-------- docs/progress.md | 14 ---- scripts/new-branch.sh | 44 ++++++++++++ 5 files changed, 335 insertions(+), 42 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100755 scripts/new-branch.sh diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..737ecccd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ + +# TECHLETES Code of Conduct + +## Our Commitment + +As employees and contributors at TECHLETES, we are committed to maintaining a professional, respectful, and harassment-free work environment for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that foster a positive, inclusive, and healthy workplace. + + +## Standards of Conduct + +Expected behaviors include: +- Demonstrating empathy and kindness toward colleagues +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing for mistakes, and learning from them +- Focusing on what is best for TECHLETES and our clients + +Unacceptable behaviors include: +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing othersโ€™ private information, such as a physical or email address, without explicit permission +- Any conduct which could reasonably be considered inappropriate in a professional setting + + +## Enforcement Responsibilities + +TECHLETES project maintainers and team leads are responsible for clarifying and enforcing these standards of acceptable behavior. They will take appropriate and fair corrective action in response to any behavior deemed inappropriate, threatening, offensive, or harmful. + +Project maintainers and team leads have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies to all TECHLETES employees and contributors working on this project, whether in person or online, and in all project-related spaces. + + +## Reporting and Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to your TECHLETES team lead or project maintainer at [INSERT CONTACT EMAIL]. + +All complaints will be reviewed and investigated promptly and fairly, with respect for the privacy and security of the reporter. + + +## Enforcement Guidelines + +TECHLETES project maintainers and team leads will follow these guidelines in determining consequences for violations: + +### 1. Correction + +**Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the workplace. + +**Consequence**: A private, written warning from a project maintainer or team lead, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. Violating these terms may lead to a temporary or permanent removal from the project. + +### 3. Temporary Removal + +**Impact**: A serious violation of standards, including sustained inappropriate behavior. + +**Consequence**: Temporary removal from the project or project-related communication for a specified period. Violating these terms may lead to permanent removal. + +### 4. Permanent Removal + +**Impact**: Demonstrating a pattern of violation of standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: Permanent removal from the project and all related activities. + + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..50bff42e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,160 @@ + + +# Contributing to python_basic_template (TECHLETES) + +Thank you for your interest in contributing! This repository serves as the official Python project template for TECHLETES, a data & AI consultancy. All contributors are TECHLETES employees, and contributions should reflect our standards of professionalism, quality, and collaboration. + + +## Prerequisite: 1Password CLI Setup + +Before starting, you must set up the 1Password CLI for secret management. This is required for all TECHLETES Python projects and for running the setup scripts. + +### 0.1.1 Set-up 1Password (CLI) + +1. **Download and activate 1Password Desktop** + 1. Download and install from here: + [Download 1Password for Windows | 1Password](https://1password.com/downloads/windows) + 2. Log in with your account. + 3. Go to โ€˜Settingsโ€™ > โ€˜Securityโ€™ > Enable โ€˜Unlock with Windows Helloโ€™ + 4. Go to โ€˜Settingsโ€™ > โ€˜Developerโ€™ > Enable โ€˜Integrate with 1Password CLIโ€™. + +2. **Set up 1Password CLI** + 1. Open Windows PowerShell and run: + ```bash + winget install 1password-cli + ``` + 2. In your file explorer, navigate to: + `C:/Users//AppData/Local/Microsoft/WinGet/Packages/<1password_cli_package_id>/op.exe` and copy the absolute path to `op.exe`. + 3. Modify the path by replacing `C:/` with `/mnt/c/` and all backslashes with forward slashes. Your final path should look like: + `/mnt/c/Users/thome/AppData/Local/Microsoft/WinGet/Packages/AgileBits.1Password.CLI_Microsoft.Winget.Source_8wekyb3d8bbwe/op.exe` + 4. In WSL, run: + ```bash + ln -s /usr/bin/op + ``` + 5. Test in WSL: + ```bash + op --version + ``` + If you see a version number, the link was successful and you can access 1Password in WSL. + + + +## Getting Started + +1. **Clone the repository** (forking is not required for internal TECHLETES projects). +2. **Automatic setup (recommended):** + After cloning, run the setup script to configure your environment: + ```bash + bash scripts/setup.sh + ``` + This script will: + - Check for Python 3.12+, pip, curl, and git + - Create and activate a virtual environment + - Install and lock all dependencies (prod & dev) + - Set up pre-commit hooks + - Check and configure system locale + - Install and configure direnv (if available) + - Initialize secret scanning baseline + - Validate your environment and dependencies + For a detailed breakdown or manual steps, see `docs/0_setup.md`. +3. **Set up pre-commit hooks** (required): + ```bash + pre-commit install + ``` + + +## Code Quality & Standards + +- All code must pass pre-commit hooks (formatting, linting, type checks, secret scanning, etc.). +- Use type annotations wherever possible. +- Write clear, concise docstrings for public functions and classes. +- Keep functions small and focused. +- Follow TECHLETES best practices for code, security, and documentation. + + + +## Making Changes + +### 1. Create a new branch (required) + +Use the provided script to create a new feature, fix, or docs branch: + +```bash +./scripts/new-branch.sh feature "add user dashboard" +# or for a bugfix: +./scripts/new-branch.sh fix "sidebar mobile issue" +``` + +This script will: +- Clean up merged local branches +- Prune remote-tracking branches +- Create a new branch with a safe, descriptive name +- Switch you to the new branch and print next steps + +**Branch types supported:** feature, fix, docs, ui, devops + +### 2. Write tests for new features or bugfixes + +### 3. Run all checks locally + +```bash +pre-commit run --all-files +pytest +``` + +### 4. Update documentation if your change affects usage or APIs + + + +## Commit Messages + +- Use clear, descriptive commit messages. +- Reference issues or pull requests when relevant. +- For new branches, follow the script's suggestion: + - `feature: add user dashboard` + - `fix: sidebar mobile issue` +- Follow any additional TECHLETES commit or PR guidelines as required. + + +## Pull Requests + +- Ensure your branch is up to date with `main`. +- All checks must pass before merging. +- Be responsive to code review feedback. +- Pull requests should be reviewed by at least one other TECHLETES team member. + + + +## Reporting Issues + +- Use the GitHub Issues page to report bugs or request features. +- Provide as much detail as possible, including steps to reproduce. +- For internal issues, you may also escalate via TECHLETES channels as appropriate. + +--- + +## Reference: Project Setup Steps (Summary) + +For full details, see `docs/0_setup.md`. Key steps include: + +1. **System Requirements**: Python 3.12+, pip, curl, git +2. **Virtual Environment**: Remove old `venv`, create and activate new one +3. **Dependency Management**: Use `scripts/dependency.sh` to install prod/dev dependencies and lock files +4. **Locale Configuration**: Ensure `en_US.UTF-8` is available +5. **direnv Setup**: (Optional) For automatic environment variable loading +6. **Pre-commit Hooks**: Install and update hooks, run all checks +7. **Secret Management**: Set up 1Password CLI and initialize `detect-secrets` baseline +8. **Tool Verification**: Confirm Black, Ruff, Mypy, Beartype, and pytest are installed +9. **Project Validation**: Import modules, syntax check all Python files, verify dependencies + +All of these are automated by `scripts/setup.sh`. + + +## Workplace Standards + +- Be respectful and constructive in all communications with TECHLETES colleagues. +- Follow the [TECHLETES Code of Conduct](CODE_OF_CONDUCT.md). + +--- + +Thank you for helping make this project better! diff --git a/README.md b/README.md index 410a59fa..c6c849b1 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,18 @@ -# Python Basic Template -A modern, production-ready Python project template with hybrid dependency management, secret management, and comprehensive development tooling. +# Python Basic Template (TECHLETES) + +A modern, production-ready Python project template for TECHLETES, a data & AI consultancy. Includes hybrid dependency management, secure secret management, and comprehensive development tooling for internal use by TECHLETES employees. ## ๐Ÿš€ Quick Start + ### Prerequisites - Python 3.12 or later - git - curl - sudo access (for system package installation) +- 1Password CLI (see [docs/0_setup.md](docs/0_setup.md) for setup instructions) ### One-Command Setup @@ -33,9 +36,11 @@ This will set up everything you need for development! - **detect-secrets** for preventing secrets in git - **Environment variable management** with direnv + ### ๐Ÿ› ๏ธ **Development Tools** -- **Code formatting**: Black, isort -- **Linting**: flake8, mypy +- **Code formatting**: Black +- **Linting & import sorting**: Ruff (replaces flake8, isort, pyupgrade) +- **Type checking**: mypy & beartype - **Testing**: pytest with coverage - **Pre-commit hooks** for code quality - **Jupyter notebook support** with nbstripout @@ -43,23 +48,42 @@ This will set up everything you need for development! ### ๐Ÿ“ฆ **Project Structure** ``` python_basic_template/ -โ”œโ”€โ”€ docs/ # Documentation -โ”‚ โ”œโ”€โ”€ dependency_management.md # Dependency workflow guide -โ”‚ โ””โ”€โ”€ secret_management.md # Secret management guide -โ”œโ”€โ”€ example/ # Example code -โ”‚ โ”œโ”€โ”€ using_secrets.py # Secret management examples -โ”‚ โ””โ”€โ”€ using_secrets.ipynb # Jupyter notebook examples +โ”œโ”€โ”€ .github/ +โ”‚ โ””โ”€โ”€ workflows/ # GitHub Actions workflows +โ”œโ”€โ”€ docs/ # Documentation (setup, secrets, dependencies, quality, progress) +โ”‚ โ”œโ”€โ”€ 0_setup.md +โ”‚ โ”œโ”€โ”€ 1_secret_management.md +โ”‚ โ”œโ”€โ”€ 2_dependency_management.md +โ”‚ โ”œโ”€โ”€ 3_pre_commit_hooks.md +โ”‚ โ”œโ”€โ”€ 4_code_quality.md +โ”‚ โ””โ”€โ”€ progress.md +โ”œโ”€โ”€ example/ # Example code and notebooks +โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”œโ”€โ”€ using_secrets.py +โ”‚ โ””โ”€โ”€ using_secrets.ipynb โ”œโ”€โ”€ scripts/ # Setup and utility scripts -โ”‚ โ”œโ”€โ”€ setup.sh # Complete development setup -โ”‚ โ”œโ”€โ”€ dependency.sh # Dependency management -โ”‚ โ””โ”€โ”€ hooks/ # Git hooks +โ”‚ โ”œโ”€โ”€ setup.sh +โ”‚ โ”œโ”€โ”€ dependency.sh +โ”‚ โ”œโ”€โ”€ new-branch.sh +โ”‚ โ””โ”€โ”€ hooks/ +โ”‚ โ”œโ”€โ”€ black-autoformat.sh +โ”‚ โ”œโ”€โ”€ check-requirements.sh +โ”‚ โ””โ”€โ”€ nbstripout-autoadd.sh โ”œโ”€โ”€ utils/ # Utility modules -โ”‚ โ”œโ”€โ”€ secrets.py # Secret management utilities -โ”‚ โ””โ”€โ”€ utils.py # General utilities -โ”œโ”€โ”€ .github/workflows/ # GitHub Actions +โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”œโ”€โ”€ secrets.py +โ”‚ โ””โ”€โ”€ utils.py +โ”œโ”€โ”€ tests/ # (Empty) Test directory scaffold โ”œโ”€โ”€ pyproject.toml # Project configuration & dev dependencies -โ”œโ”€โ”€ requirements.in # Production dependencies -โ””โ”€โ”€ requirements.txt # Compiled production dependencies +โ”œโ”€โ”€ requirements.txt # Compiled production dependencies +โ”œโ”€โ”€ requirements-dev.txt # Compiled dev dependencies +โ”œโ”€โ”€ .pre-commit-config.yaml # Pre-commit hooks config +โ”œโ”€โ”€ .envrc # direnv environment config +โ”œโ”€โ”€ .secrets.baseline # Secret detection baseline +โ”œโ”€โ”€ CODE_OF_CONDUCT.md # Contributor code of conduct +โ”œโ”€โ”€ CONTRIBUTING.md # Contribution guidelines +โ”œโ”€โ”€ setup.py # (Optional) Legacy setup script +โ””โ”€โ”€ README.md # Project overview (this file) ``` ## ๐Ÿ—๏ธ Dependency Management @@ -137,15 +161,16 @@ git add pyproject.toml git commit -m "Add development tool" ``` +mypy . + ### Code Quality ```bash # Format code black . -isort . -# Lint code -flake8 . +# Lint and fix code +ruff check . --fix mypy . # Run tests @@ -212,16 +237,17 @@ All development tools are configured in `pyproject.toml`: ## ๐Ÿ“š Documentation -- **[Dependency Management](docs/dependency_management.md)** - Detailed dependency workflow -- **[Secret Management](docs/secret_management.md)** - Secure secret handling guide +0. **[Setup](docs/0_setup.md)** - Guide to setup the development environment +1. **[Secret Management](docs/3_secret_management.md)** - Secure secret handling guide +2. **[Dependency Management](docs/2_dependency_management.md)** - Detailed dependency workflow +3. **[Pre Commit Hooks](docs/3_pre_commit_hooks.md)** - Pre commit hooks to ensure safety and quality +4. **[Code Quality](docs/4_code_quality.md)** - Rules and guidelines on code quality and how it is enforced + + ## ๐Ÿค Contributing -1. **Setup development environment**: `./scripts/setup.sh` -2. **Create feature branch**: `git checkout -b feature/my-feature` -3. **Make changes** and ensure tests pass -4. **Pre-commit hooks** will run automatically -5. **Submit pull request** +This template is for use by TECHLETES employees. See [CONTRIBUTING.md](CONTRIBUTING.md) for internal contribution guidelines, required 1Password CLI setup, and branch workflow. ## ๐Ÿ“„ License diff --git a/docs/progress.md b/docs/progress.md index a015f940..87f8cf47 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -59,13 +59,11 @@ Below are subjectโ€‘specific action items to further enhance our DevOps maturity * Add a CI job to validate `requirements.txt` against `requirements.in` (`pip-compile --dry-run`) * Enable Dependabot or Renovate to automate dependency updates -* Consider adopting **PEPย 621** metadata in `pyproject.toml` for standardized packaging ## 2.3 Code Quality * Enforce **test coverage thresholds** (e.g., `pytest --cov`, coverage badge) * Introduce **commit message linting** (e.g., conventional commits with `commitlint`) -* Add **CODEOWNERS** and require reviews from code owners in branch rules ## 2.4 CI/CD @@ -74,18 +72,6 @@ Below are subjectโ€‘specific action items to further enhance our DevOps maturity * Integrate **release automation** (autoโ€‘generate changelogs, tag releases) * Cache dependencies and artifacts to speed up pipelines -## 2.5 Packaging & Distribution - -* Scaffold `setup.py` / `pyproject.toml` with metadata for publishing -* Configure GitHub Packages or private PyPI for internal distribution -* Automate packaging & publish step in CI upon tag release - -## 2.6 Observability & Monitoring - -* Provide a logging and metrics template (e.g., `structlog`, Prometheus client) -* Integrate Sentry or similar error monitoring in template -* Add a healthโ€‘check endpoint stub for service readiness/liveness - ## 2.7 Infrastructure as Code * Include a **Dockerfile** template following best practices (multiโ€‘stage, minimal base) diff --git a/scripts/new-branch.sh b/scripts/new-branch.sh new file mode 100755 index 00000000..c8f95b49 --- /dev/null +++ b/scripts/new-branch.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Quick script to start a new feature branch +# Usage: ./scripts/new-feature.sh feature "responsive navigation" +# Usage: ./scripts/new-feature.sh fix "sidebar mobile issue" + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "Types: feature, fix, docs, ui, devops" + echo "Example: $0 feature 'add user dashboard'" + exit 1 +fi + +TYPE=$1 +DESCRIPTION=$2 + +# Convert description to branch-safe format +BRANCH_NAME=$(echo "$DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') + +FULL_BRANCH="${TYPE}/${BRANCH_NAME}" + +echo "cleaning up old branches" +# Prune remote-tracking branches (remove references to remote branches deleted on GitHub) + +git remote prune origin +echo "Pruned remote-tracking branches" + +# (Optional) Delete all local branches that have been merged into main (except main itself) +(git branch --merged main | grep -v "main" | xargs -n 1 git branch -d) || true +echo "Removed merged local branches" + +echo "Creating branch: $FULL_BRANCH" + + +# Create and switch to new branch from current HEAD (preserves local changes) +git checkout -b "$FULL_BRANCH" + +echo "โœ… Ready to work on: $FULL_BRANCH" +echo "" +echo "When you're done:" +echo "1. git add ." +echo "2. git commit -m \"${TYPE}: ${DESCRIPTION}\"" +echo "3. git push origin $FULL_BRANCH" +echo "4. Create PR on GitHub" From 207ed817be7b3ee86a9ebc49db821eebc861a054 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 14:43:54 +0200 Subject: [PATCH 022/199] Update README --- README.md | 79 ++++++++++++++++++++----------------------------------- 1 file changed, 28 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index c6c849b1..f3290288 100644 --- a/README.md +++ b/README.md @@ -90,24 +90,30 @@ python_basic_template/ This project uses a **hybrid dependency management approach**: -### Production Dependencies -- Add to `requirements.in` -- Compiled to `requirements.txt` via pip-tools -- Install with: `pip install -r requirements.txt` -### Development Dependencies -- Defined in `pyproject.toml` under `[project.optional-dependencies.dev]` -- Install with: `pip install -e .[dev]` +### Production dependencies +- Declared under `dependencies` in `pyproject.toml` +- Locked to `requirements.txt` via pip-tools + +### Development dependencies +- Declared under `[project.optional-dependencies].dev` in `pyproject.toml` +- Locked to `requirements-dev.txt` via pip-tools + +This setup ensures: + +- Clear separation of runtime vs. tooling packages +- Fully pinned, reproducible installs using lockfiles +- One declarative manifest (`pyproject.toml`) for all dependencies ### Installation Options | Command | What it installs | |---------|------------------| -| `pip install -e .` | Minimal (production only) | -| `pip install -e .[dev]` | Full development setup | -| `./scripts/dependency.sh` | Both production + development | +| `./scripts/dependency.sh --prod` | Production setup only | +| `./scripts/dependency.sh --dev` | Development setup only | +| `./scripts/dependency.sh` | Both production + development (Default) | -**๐Ÿ“– Detailed guide**: [docs/dependency_management.md](docs/dependency_management.md) +**๐Ÿ“– Detailed guide**: [docs/2_dependency_management.md](docs/2_dependency_management.md) ## ๐Ÿ” Secret Management @@ -116,9 +122,9 @@ Secure secret handling with 1Password CLI integration: - **Environment variables** loaded via direnv - **1Password CLI** for secure secret retrieval - **detect-secrets** prevents accidental commits -- **Example usage** in `example/using_secrets.py` +- **Example usage** in [example/using_secrets.py](example/using_secrets.py) -**๐Ÿ“– Detailed guide**: [docs/secret_management.md](docs/secret_management.md) +**๐Ÿ“– Detailed guide**: [docs/1_secret_management.md](docs/1_secret_management.md) ## ๐Ÿ› ๏ธ Development Workflow @@ -127,45 +133,16 @@ Secure secret handling with 1Password CLI integration: ```bash # Complete setup (recommended for new developers) ./scripts/setup.sh - -# Or manual setup -python3 -m venv venv -source venv/bin/activate -pip install -e .[dev] -pre-commit install ``` -### Adding Dependencies - -**Production dependency:** -```bash -# 1. Add to requirements.in -echo "requests" >> requirements.in - -# 2. Compile and install -./scripts/dependency.sh +**For manual set-up see**: [docs/0_setup.md](docs/0_setup.md) -# 3. Commit both files -git add requirements.in requirements.txt -git commit -m "Add requests dependency" -``` +### Code Quality -**Development tool:** ```bash -# 1. Add to pyproject.toml [project.optional-dependencies.dev] -# 2. Install -pip install -e .[dev] - -# 3. Commit -git add pyproject.toml -git commit -m "Add development tool" -``` - +# Typing check mypy . -### Code Quality - -```bash # Format code black . @@ -201,9 +178,9 @@ pytest tests/test_specific.py | File | Purpose | |------|---------| -| `pyproject.toml` | Project metadata, dev dependencies, tool config | -| `requirements.in` | Production dependencies (human-edited) | +| `pyproject.toml` | Project metadata, (dev) dependencies, tool config | | `requirements.txt` | Compiled production dependencies (auto-generated) | +| `requirements-dev.txt` | Compiled development dependencies (auto-generated) | | `.pre-commit-config.yaml` | Pre-commit hooks configuration | | `.envrc` | Environment variables (direnv) | | `.secrets.baseline` | Secret detection baseline | @@ -212,10 +189,10 @@ pytest tests/test_specific.py All development tools are configured in `pyproject.toml`: - **Black**: Code formatting (88 char line length) -- **isort**: Import sorting (Black compatible) +- **ruff**: Sorting, linting, formatting (replaces flake8, isort, pyupgrade) - **pytest**: Test configuration with coverage - **mypy**: Type checking -- **flake8**: Linting rules +- **beartype**: Type checking at runtime (more extensive then mypy) ## ๐Ÿš€ Using This Template @@ -230,7 +207,7 @@ All development tools are configured in `pyproject.toml`: ### Customization -- **Add your production dependencies** to `requirements.in` +- **Add your production dependencies** to `pyproject.toml` - **Modify tool configurations** in `pyproject.toml` - **Update documentation** in `docs/` - **Add your modules** alongside `utils/` @@ -238,7 +215,7 @@ All development tools are configured in `pyproject.toml`: ## ๐Ÿ“š Documentation 0. **[Setup](docs/0_setup.md)** - Guide to setup the development environment -1. **[Secret Management](docs/3_secret_management.md)** - Secure secret handling guide +1. **[Secret Management](docs/1_secret_management.md)** - Secure secret handling guide 2. **[Dependency Management](docs/2_dependency_management.md)** - Detailed dependency workflow 3. **[Pre Commit Hooks](docs/3_pre_commit_hooks.md)** - Pre commit hooks to ensure safety and quality 4. **[Code Quality](docs/4_code_quality.md)** - Rules and guidelines on code quality and how it is enforced From 9263966bf3b3b75c6f9ae28172e8bdbffb4d0802 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 15:55:53 +0200 Subject: [PATCH 023/199] Setup pytest --- .detect-secrets.yaml | 3 + .github/workflows/ci.yml.example | 57 +++++++++---- .pre-commit-config.yaml | 8 ++ docs/progress.md | 3 - example/using_secrets.py | 20 +++-- pyproject.toml | 9 +- requirements-dev.txt | 140 ++++++++++++++++++++++++++++++- tests/utils/test_secrets.py | 41 +++++++++ utils/utils.py | 13 --- 9 files changed, 252 insertions(+), 42 deletions(-) create mode 100644 .detect-secrets.yaml create mode 100644 tests/utils/test_secrets.py delete mode 100644 utils/utils.py diff --git a/.detect-secrets.yaml b/.detect-secrets.yaml new file mode 100644 index 00000000..5a25d1cc --- /dev/null +++ b/.detect-secrets.yaml @@ -0,0 +1,3 @@ +exclude: + files: + - ^tests/ \ No newline at end of file diff --git a/.github/workflows/ci.yml.example b/.github/workflows/ci.yml.example index 103d6f7f..3559071c 100644 --- a/.github/workflows/ci.yml.example +++ b/.github/workflows/ci.yml.example @@ -3,34 +3,61 @@ name: CI on: - push: + pull_request: branches: - main - staging - pull_request: jobs: - test: - name: Run Tests + lint-and-test: + name: Lint & Test runs-on: ubuntu-latest - - env: - API_KEY: ${{ secrets.API_KEY }} - DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - + strategy: + matrix: + python-version: ['3.12'] steps: - name: Checkout code uses: actions/checkout@v3 - - name: Set up Python + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: - python-version: '3.12' + python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install pip-tools + run: pip install pip-tools + + - name: Validate lockfiles run: | - pip install -r requirements.txt + pip-compile --dry-run --generate-hashes \ + --output-file=requirements.txt pyproject.toml + pip-compile --dry-run --generate-hashes --extra=dev \ + --output-file=requirements-dev.txt pyproject.toml - - name: Run tests + - name: Install dependencies run: | - pytest + pip install --require-hashes -r requirements.txt + pip install --require-hashes -r requirements-dev.txt + + - name: Run pip-audit + run: pip-audit --fail-on high + + - name: Black formatting check + run: black --check . + + - name: Ruff linting + run: ruff . + + - name: Type checking with mypy + run: mypy . + + - name: Run tests with pytest + run: pytest --cov diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 59468ae7..d585b631 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,6 +24,13 @@ repos: additional_dependencies: [black] types: [python] + # Dependency vulnerability scanning + - repo: https://github.com/pypa/pip-audit + rev: v2.9.0 + hooks: + - id: pip-audit + args: ['--progress-spinner', 'off'] + stages: [pre-commit] # Secrets scanning - repo: https://github.com/Yelp/detect-secrets @@ -31,6 +38,7 @@ repos: hooks: - id: detect-secrets args: ['--baseline', '.secrets.baseline'] + exclude: ^tests/ # Notebook/text sync - repo: https://github.com/mwouts/jupytext diff --git a/docs/progress.md b/docs/progress.md index 87f8cf47..d1b11b50 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -51,13 +51,11 @@ Below are subjectโ€‘specific action items to further enhance our DevOps maturity ## 2.1 Secrets Management * Add automated secret rotation scan and enforcement in CI (e.g., 1Password rotation policy check) -* Integrate **`pip-audit`** or **`safety`** for dependency vulnerability scanning * Evaluate centralized secrets store (e.g., Vault) for dynamic credentials in CI/CD * Enforce environmentโ€‘specific secret vaults (dev, staging, prod) ## 2.2 Dependency Management -* Add a CI job to validate `requirements.txt` against `requirements.in` (`pip-compile --dry-run`) * Enable Dependabot or Renovate to automate dependency updates ## 2.3 Code Quality @@ -80,7 +78,6 @@ Below are subjectโ€‘specific action items to further enhance our DevOps maturity ## 2.8 Documentation & Onboarding -* Create `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, and `README.md` badges for code quality status * Automate documentation generation (e.g., Sphinx/MkDocs) in CI * Provide Notion templates or a docs portal scaffold diff --git a/example/using_secrets.py b/example/using_secrets.py index 8edbe43c..5d413e13 100644 --- a/example/using_secrets.py +++ b/example/using_secrets.py @@ -1,9 +1,15 @@ from utils.secrets import get_secret -# Use the get_secret function to retrieve your secret. -# To find the path to your secret go to 1Password Interace > your item > click dropdown next to field > "Copy secret reference" -api_key = get_secret("op://Shared with all/RAGFLow API Key/credential") -if api_key: - print(f"API_KEY is set: {api_key}") -else: - print("API_KEY is not set.") + +def main() -> None: + # Use the get_secret function to retrieve your secret. + # To find the path to your secret go to 1Password Interace > your item > click dropdown next to field > "Copy secret reference" + api_key = get_secret("op://Shared with all/RAGFLow API Key/credential") + if api_key: + print(f"API_KEY is set: {api_key}") + else: + print("API_KEY is not set.") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 11adbbb9..8ac66489 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,8 @@ dev = [ "ruff", "beartype", "pytest-beartype", - "pip-tools", # for managing requirements.in/txt + "pip-audit>=2.9.0", + "pip-tools", # for managing requirement*.txt ] [project.urls] @@ -79,9 +80,11 @@ python_functions = ["test_*"] [tool.coverage.run] source = ["utils", "example"] omit = [ + "*/__pycache__/*", "*/tests/*", + "*/.venv/*", "*/venv/*", - "*/__pycache__/*", + "*/__init__.py", "setup.py", ] @@ -97,6 +100,8 @@ exclude_lines = [ "if __name__ == .__main__.:", "class .*\\bProtocol\\):", "@(abc\\.)?abstractmethod", + "except ImportError", + "pass", ] [tool.mypy] diff --git a/requirements-dev.txt b/requirements-dev.txt index 82fcd913..fce726f7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -44,10 +44,20 @@ blinker==1.9.0 \ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc # via flask +boolean-py==5.0 \ + --hash=sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95 \ + --hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9 + # via license-expression build==1.3.0 \ --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 # via pip-tools +cachecontrol[filecache]==0.14.3 \ + --hash=sha256:73e7efec4b06b20d9267b441c1f733664f989fb8688391b670ca812d70795d11 \ + --hash=sha256:b35e44a3113f17d2a31c1e6b27b9de6d4405f84ae51baa8c1d3cc5b633010cae + # via + # cachecontrol + # pip-audit certifi==2025.8.3 \ --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 @@ -247,6 +257,14 @@ coverage[toml]==7.10.2 \ --hash=sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947 \ --hash=sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb # via pytest-cov +cyclonedx-python-lib==9.1.0 \ + --hash=sha256:55693fca8edaecc3363b24af14e82cc6e659eb1e8353e58b587c42652ce0fb52 \ + --hash=sha256:86935f2c88a7b47a529b93c724dbd3e903bc573f6f8bd977628a7ca1b5dadea1 + # via pip-audit +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via py-serializable detect-secrets==1.5.0 \ --hash=sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a \ --hash=sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060 @@ -262,7 +280,9 @@ fastjsonschema==2.21.1 \ filelock==3.18.0 \ --hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \ --hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de - # via virtualenv + # via + # cachecontrol + # virtualenv flask==2.3.3 \ --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b @@ -299,6 +319,14 @@ jupyter-core==5.8.1 \ --hash=sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941 \ --hash=sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0 # via nbformat +license-expression==30.4.4 \ + --hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \ + --hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd + # via cyclonedx-python-lib +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb + # via rich markupsafe==3.0.2 \ --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \ --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ @@ -364,6 +392,71 @@ markupsafe==3.0.2 \ # via # jinja2 # werkzeug +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +msgpack==1.1.1 \ + --hash=sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8 \ + --hash=sha256:1abfc6e949b352dadf4bce0eb78023212ec5ac42f6abfd469ce91d783c149c2a \ + --hash=sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90 \ + --hash=sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf \ + --hash=sha256:2fbbc0b906a24038c9958a1ba7ae0918ad35b06cb449d398b76a7d08470b0ed9 \ + --hash=sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157 \ + --hash=sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed \ + --hash=sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d \ + --hash=sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0 \ + --hash=sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232 \ + --hash=sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084 \ + --hash=sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5 \ + --hash=sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1 \ + --hash=sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88 \ + --hash=sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752 \ + --hash=sha256:4d3237b224b930d58e9d83c81c0dba7aacc20fcc2f89c1e5423aa0529a4cd142 \ + --hash=sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac \ + --hash=sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef \ + --hash=sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323 \ + --hash=sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4 \ + --hash=sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458 \ + --hash=sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57 \ + --hash=sha256:61e35a55a546a1690d9d09effaa436c25ae6130573b6ee9829c37ef0f18d5e78 \ + --hash=sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd \ + --hash=sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69 \ + --hash=sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce \ + --hash=sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558 \ + --hash=sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd \ + --hash=sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2 \ + --hash=sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8 \ + --hash=sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0 \ + --hash=sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295 \ + --hash=sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c \ + --hash=sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26 \ + --hash=sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2 \ + --hash=sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f \ + --hash=sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4 \ + --hash=sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8 \ + --hash=sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9 \ + --hash=sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338 \ + --hash=sha256:996f2609ddf0142daba4cefd767d6db26958aac8439ee41db9cc0db9f4c4c3a6 \ + --hash=sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a \ + --hash=sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0 \ + --hash=sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a \ + --hash=sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478 \ + --hash=sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238 \ + --hash=sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7 \ + --hash=sha256:b8f93dcddb243159c9e4109c9750ba5b335ab8d48d9522c5308cd05d7e3ce600 \ + --hash=sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704 \ + --hash=sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a \ + --hash=sha256:bba1be28247e68994355e028dcd668316db30c1f758d3241a7b903ac78dcd285 \ + --hash=sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c \ + --hash=sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf \ + --hash=sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b \ + --hash=sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2 \ + --hash=sha256:da8f41e602574ece93dbbda1fab24650d6bf2a24089f9e9dbb4f5730ec1e58ad \ + --hash=sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b \ + --hash=sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b \ + --hash=sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75 + # via cachecontrol mypy==1.17.1 \ --hash=sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341 \ --hash=sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5 \ @@ -422,12 +515,18 @@ nodeenv==1.9.1 \ --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 # via pre-commit +packageurl-python==0.17.3 \ + --hash=sha256:719995f0c7f706890277ba57ec95afcaa9696c836a7675770a1279b01a41f7be \ + --hash=sha256:f51b5aab570159f07258c8e998e9972ff3bf060da16b7334a42bd9f9737777d9 + # via cyclonedx-python-lib packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f # via # black # build + # pip-audit + # pip-requirements-parser # pytest pathspec==0.12.1 \ --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ @@ -435,6 +534,18 @@ pathspec==0.12.1 \ # via # black # mypy +pip-api==0.0.34 \ + --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ + --hash=sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625 + # via pip-audit +pip-audit==2.9.0 \ + --hash=sha256:0b998410b58339d7a231e5aa004326a294e4c7c6295289cdc9d5e1ef07b1f44d \ + --hash=sha256:348b16e60895749a0839875d7cc27ebd692e1584ebe5d5cb145941c8e25a80bd + # via python-basic-template (pyproject.toml) +pip-requirements-parser==32.0.1 \ + --hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \ + --hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3 + # via pip-audit pip-tools==7.5.0 \ --hash=sha256:30639f50961bb09f49d22f4389e8d7d990709677c094ce1114186b1f2e9b5821 \ --hash=sha256:69758e4e5a65f160e315d74db46246fdbb30d549f1ed0c4236d057122c9b0f18 @@ -445,6 +556,7 @@ platformdirs==4.3.8 \ # via # black # jupyter-core + # pip-audit # virtualenv pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ @@ -456,10 +568,20 @@ pre-commit==4.2.0 \ --hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \ --hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd # via python-basic-template (pyproject.toml) +py-serializable==2.1.0 \ + --hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \ + --hash=sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304 + # via cyclonedx-python-lib pygments==2.19.2 \ --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b - # via pytest + # via + # pytest + # rich +pyparsing==3.2.3 \ + --hash=sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf \ + --hash=sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be + # via pip-requirements-parser pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 @@ -548,8 +670,14 @@ requests==2.32.4 \ --hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \ --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422 # via + # cachecontrol # detect-secrets + # pip-audit # python-basic-template (pyproject.toml) +rich==14.1.0 \ + --hash=sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f \ + --hash=sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8 + # via pip-audit rpds-py==0.26.0 \ --hash=sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3 \ --hash=sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b \ @@ -718,6 +846,14 @@ ruff==0.12.7 \ --hash=sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69 \ --hash=sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8 # via python-basic-template (pyproject.toml) +sortedcontainers==2.4.0 \ + --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ + --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 + # via cyclonedx-python-lib +toml==0.10.2 \ + --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ + --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f + # via pip-audit traitlets==5.14.3 \ --hash=sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7 \ --hash=sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f diff --git a/tests/utils/test_secrets.py b/tests/utils/test_secrets.py new file mode 100644 index 00000000..a37b32f0 --- /dev/null +++ b/tests/utils/test_secrets.py @@ -0,0 +1,41 @@ +import os +import subprocess + +import pytest +from pytest import MonkeyPatch + +from utils import secrets + + +def test_get_secret_sets_env_var(monkeypatch: MonkeyPatch) -> None: + # Patch subprocess.check_output to return a fake secret + + def fake_check_output(args: list[str]) -> bytes: + return b"supersecret\n" + + monkeypatch.setattr( + subprocess, + "check_output", + fake_check_output, + ) + # Ensure env var is not set before + env_var = "TEST_SECRET_ENV" + if env_var in os.environ: + del os.environ[env_var] + secret = secrets.get_secret("op://Vault/Item/field", env_var=env_var) + assert secret == "supersecret" + assert os.environ[env_var] == "supersecret" + + +def test_get_secret_invalid_path() -> None: + with pytest.raises(ValueError): + secrets.get_secret("invalid_path") + + +def test_get_secret_no_env_var(monkeypatch: MonkeyPatch) -> None: + def fake_nosecret(args: list[str]) -> bytes: + return b"nosecret\n" + + monkeypatch.setattr(subprocess, "check_output", fake_nosecret) + secret = secrets.get_secret("op://Vault/Item/field") + assert secret == "nosecret" diff --git a/utils/utils.py b/utils/utils.py deleted file mode 100644 index 7aa1b998..00000000 --- a/utils/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -import sys -from pathlib import Path - - -def add_project_root(marker: str = ".git") -> None: - path = Path(__file__).resolve() - for parent in path.parents: - if (parent / marker).exists(): - sys.path.append(str(parent)) - return - raise RuntimeError( - f"Project root not found (no '{marker}' in any parent directories)" - ) From 7017e8be8f88f5ba332a3ddc26ba27391dd280ae Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 16:20:56 +0200 Subject: [PATCH 024/199] Add pytest hook to pre-commit configuration for test coverage --- .pre-commit-config.yaml | 26 ++++++++++++++++++++++++++ pyproject.toml | 7 ++++--- scripts/hooks/precommit_pytest.sh | 11 +++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100755 scripts/hooks/precommit_pytest.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d585b631..d0ee4588 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,3 +63,29 @@ repos: name: Ruff (linting + extra isort + rules) args: [--fix] types: [python] + + # - repo: local + # hooks: + # - id: pytest + # name: Run pytest with coverage & beartype + # entry: pytest --cov --cov-fail-under=80 --beartype-packages=utils,example + # language: python + # additional_dependencies: + # - pytest + # - pytest-cov + # - pytest-beartype + # pass_filenames: false + # always_run: true + # stages: [pre-commit] + + - repo: local + hooks: + - id: pytest + name: Run pytest with coverage & beartype + entry: bash scripts/hooks/precommit_pytest.sh + language: system + pass_filenames: false + always_run: true + stages: [pre-commit] + + diff --git a/pyproject.toml b/pyproject.toml index 8ac66489..06b3a675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,9 +47,9 @@ dev = [ ] [project.urls] -Homepage = "https://github.com/thom-techlete/python_basic_template" -Repository = "https://github.com/thom-techlete/python_basic_template" -Issues = "https://github.com/thom-techlete/python_basic_template/issues" +Homepage = "https://github.com/TECHLETES/python_basic_template" +Repository = "https://github.com/TECHLETES/python_basic_template" +Issues = "https://github.com/TECHLETES/python_basic_template/issues" [tool.setuptools.packages.find] where = ["."] @@ -82,6 +82,7 @@ source = ["utils", "example"] omit = [ "*/__pycache__/*", "*/tests/*", + "*/example/*", "*/.venv/*", "*/venv/*", "*/__init__.py", diff --git a/scripts/hooks/precommit_pytest.sh b/scripts/hooks/precommit_pytest.sh new file mode 100755 index 00000000..dabee339 --- /dev/null +++ b/scripts/hooks/precommit_pytest.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e + +# Activate your local venv (adjust path if needed) +source venv/bin/activate + +# Install your project in editable mode with dev extras +pip install -q -e .[dev] + +# Run pytest with coverage and beartype runtime checks +pytest --cov --cov-fail-under=80 --beartype-packages=utils,example \ No newline at end of file From 5cfd0b57a1cf888961018dabbb6975cfbd19300c Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 16:21:25 +0200 Subject: [PATCH 025/199] Add comment to coverage omit section for clarity --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 06b3a675..e3c0423e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ omit = [ "*/venv/*", "*/__init__.py", "setup.py", + # Add any other files you want to exclude from test coverage ] [tool.coverage.report] From bdfbf31b68ec16d03a215d31dfb8616095185eab Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 16:28:35 +0200 Subject: [PATCH 026/199] Add CI workflow for linting, testing, and coverage reporting --- .github/workflows/{ci.yml.example => ci.yml} | 21 +++++++++++++++++--- README.md | 2 ++ 2 files changed, 20 insertions(+), 3 deletions(-) rename .github/workflows/{ci.yml.example => ci.yml} (75%) diff --git a/.github/workflows/ci.yml.example b/.github/workflows/ci.yml similarity index 75% rename from .github/workflows/ci.yml.example rename to .github/workflows/ci.yml index 3559071c..66b0abd7 100644 --- a/.github/workflows/ci.yml.example +++ b/.github/workflows/ci.yml @@ -3,6 +3,9 @@ name: CI on: + push: + branches: + - main pull_request: branches: - main @@ -46,7 +49,7 @@ jobs: run: | pip install --require-hashes -r requirements.txt pip install --require-hashes -r requirements-dev.txt - + - name: Run pip-audit run: pip-audit --fail-on high @@ -59,5 +62,17 @@ jobs: - name: Type checking with mypy run: mypy . - - name: Run tests with pytest - run: pytest --cov + - name: Run tests with coverage + run: | + pytest --cov --cov-report=term --cov-report=xml + + - name: Generate coverage badge + uses: tj-actions/coverage-badge-py@v2 + with: + coverage-file: coverage.xml + + - name: Commit badge + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: update coverage badge" + file_pattern: coverage.svg diff --git a/README.md b/README.md index f3290288..502f66c4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ +![Test Coverage](./coverage.svg) + # Python Basic Template (TECHLETES) A modern, production-ready Python project template for TECHLETES, a data & AI consultancy. Includes hybrid dependency management, secure secret management, and comprehensive development tooling for internal use by TECHLETES employees. From 4b9a1104ef65e57a020bfa4e2eafd592b541f530 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 16:29:55 +0200 Subject: [PATCH 027/199] Fix pip-audit command and update CI workflow --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66b0abd7..d31b522e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: pip install --require-hashes -r requirements-dev.txt - name: Run pip-audit - run: pip-audit --fail-on high + run: pip-audit --progress-spinner off - name: Black formatting check run: black --check . From 890e666cf26d6013d2bc416d5b7a7a910f26001f Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 16:33:50 +0200 Subject: [PATCH 028/199] Update CI workflow to use 'ruff check' and improve code formatting in Jupyter notebook --- .github/workflows/ci.yml | 2 +- example/using_secrets.ipynb | 1 + pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d31b522e..32afdf7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: run: black --check . - name: Ruff linting - run: ruff . + run: ruff check . - name: Type checking with mypy run: mypy . diff --git a/example/using_secrets.ipynb b/example/using_secrets.ipynb index 7b3f0e11..50eede97 100644 --- a/example/using_secrets.ipynb +++ b/example/using_secrets.ipynb @@ -17,6 +17,7 @@ "source": [ "import sys\n", "from pathlib import Path\n", + "\n", "sys.path.append(str(next(p for p in [Path.cwd()] + list(Path.cwd().parents) if (p / \".git\").exists())))" ] }, diff --git a/pyproject.toml b/pyproject.toml index e3c0423e..7033be88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,7 +128,7 @@ respect-gitignore = true extend-exclude = ["migrations"] # Enable popular rule sets -select = [ +lint.select = [ "E", # pycodestyle "F", # pyflakes "I", # isort @@ -137,7 +137,7 @@ select = [ ] # Rules to ignore (common ones) -ignore = [ +lint.ignore = [ "B008", # Do not perform function calls in argument defaults "E501", # Line too long (Black handles this) ] From f094d9e6d3bc99190468c1694efdd56d7ddc1359 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 16:37:02 +0200 Subject: [PATCH 029/199] Update python path for pytest --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 7033be88..df7406a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ testpaths = ["tests"] python_files = ["test_*.py", "*_test.py"] python_classes = ["Test*"] python_functions = ["test_*"] +pythonpath = ["."] [tool.coverage.run] source = ["utils", "example"] From 4477a61fdc5729aec18249b9dc990a8237cfa161 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 16:40:21 +0200 Subject: [PATCH 030/199] Update CI workflow to generate coverage badge with specified output and overwrite options; modify dependencies comment in pyproject.toml for clarity --- .github/workflows/ci.yml | 4 +++- pyproject.toml | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32afdf7e..f530b911 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,9 @@ jobs: - name: Generate coverage badge uses: tj-actions/coverage-badge-py@v2 with: - coverage-file: coverage.xml + working-directory: . + output: coverage.svg + overwrite: true - name: Commit badge uses: stefanzweifel/git-auto-commit-action@v5 diff --git a/pyproject.toml b/pyproject.toml index df7406a1..e445ede1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,7 @@ classifiers = [ # Add project dependencies here. pip-tools will read them from here, do not use a requirements file! dependencies = [ - "flask>=2.2.0,<3.0.0", - "requests>=2.28.0", + "flask>=2.2.0,<3.0.0", # Example dependency, replace with your own ] [project.optional-dependencies] From 521f0080d8f787bd9399afd5795463896da5d74d Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 16:44:35 +0200 Subject: [PATCH 031/199] Update CI workflow to add permissions and persist credentials during checkout --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f530b911..73110909 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,9 @@ on: - main - staging +permissions: + contents: write + jobs: lint-and-test: name: Lint & Test @@ -21,6 +24,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + with: + persist-credentials: true - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 From 8f28263580c51d48fb48c1046915ae8ca469de39 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 16:47:54 +0200 Subject: [PATCH 032/199] test --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73110909..18a78c9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,3 +83,4 @@ jobs: with: commit_message: "chore: update coverage badge" file_pattern: coverage.svg + #test From 710e8b39eb7b8724691674d7a96300a468319be5 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 5 Aug 2025 16:49:16 +0200 Subject: [PATCH 033/199] Update coverage badge --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18a78c9d..bd84e065 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,9 +3,6 @@ name: CI on: - push: - branches: - - main pull_request: branches: - main From 7e469038692d5f9cdf8c9300d777d8071dcc176f Mon Sep 17 00:00:00 2001 From: thom-techlete <176420111+thom-techlete@users.noreply.github.com> Date: Tue, 5 Aug 2025 14:51:32 +0000 Subject: [PATCH 034/199] chore: update coverage badge --- coverage.svg | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 coverage.svg diff --git a/coverage.svg b/coverage.svg new file mode 100644 index 00000000..e5db27cd --- /dev/null +++ b/coverage.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + From 6985256cd49c0d47793e9a3d6761bc3e1855d3dc Mon Sep 17 00:00:00 2001 From: divaanster Date: Thu, 28 Aug 2025 11:11:20 +0200 Subject: [PATCH 035/199] adds .ps1 scripts for Windows-native setup and updates readme --- .gitattributes | 5 ++ .pre-commit-windows.yaml | 62 +++++++++++++++++++++++ README.md | 11 ++++- scripts/dependency.ps1 | 74 ++++++++++++++++++++++++++++ scripts/env.ps1 | 25 ++++++++++ scripts/hooks/check-requirements.ps1 | 11 +++++ scripts/hooks/nbstripout-install.ps1 | 14 ++++++ scripts/hooks/precommit_pytest.ps1 | 3 ++ scripts/new-branch.ps1 | 52 +++++++++++++++++++ scripts/setup.ps1 | 67 +++++++++++++++++++++++++ 10 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 .pre-commit-windows.yaml create mode 100644 scripts/dependency.ps1 create mode 100644 scripts/env.ps1 create mode 100644 scripts/hooks/check-requirements.ps1 create mode 100644 scripts/hooks/nbstripout-install.ps1 create mode 100644 scripts/hooks/precommit_pytest.ps1 create mode 100644 scripts/new-branch.ps1 create mode 100644 scripts/setup.ps1 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..88f68334 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.sh eol=lf +*.ps1 eol=crlf +*.py eol=lf +*.ipynb -text diff --git a/.pre-commit-windows.yaml b/.pre-commit-windows.yaml new file mode 100644 index 00000000..66a8086f --- /dev/null +++ b/.pre-commit-windows.yaml @@ -0,0 +1,62 @@ +repos: + - repo: local + hooks: + - id: prevent-manual-requirements-edits + name: Prevent manual changes to requirements.txt + entry: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\hooks\\check-requirements.ps1 + language: system + types: [text] + + - repo: https://github.com/kynan/nbstripout + rev: 0.8.1 + hooks: + - id: nbstripout + + - repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black + + - repo: https://github.com/pypa/pip-audit + rev: v2.9.0 + hooks: + - id: pip-audit + args: ['--progress-spinner', 'off'] + stages: [pre-commit] + + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ['--baseline', '.secrets.baseline'] + exclude: ^tests/ + + - repo: https://github.com/mwouts/jupytext + rev: v1.17.2 + hooks: + - id: jupytext + args: [--sync] + require_serial: true + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.17.1 + hooks: + - id: mypy + args: ["--config-file=pyproject.toml"] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.10 + hooks: + - id: ruff + args: [--fix] + types: [python] + + - repo: local + hooks: + - id: pytest + name: Run pytest with coverage & beartype + entry: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\hooks\\precommit_pytest.ps1 + language: system + pass_filenames: false + always_run: true + stages: [pre-commit] diff --git a/README.md b/README.md index 502f66c4..e468f1d2 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co - sudo access (for system package installation) - 1Password CLI (see [docs/0_setup.md](docs/0_setup.md) for setup instructions) -### One-Command Setup +### Linux: One-Command Setup ```bash git clone https://github.com/thom-techlete/python_basic_template.git @@ -24,6 +24,15 @@ cd python_basic_template ./scripts/setup.sh ``` +### Windows setup + +```powershell +git clone https://github.com/thom-techlete/python_basic_template.git +cd python_basic_template +# If activation is blocked: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned +.\scripts\setup.ps1 +``` + This will set up everything you need for development! ## ๐Ÿ“‹ Features diff --git a/scripts/dependency.ps1 b/scripts/dependency.ps1 new file mode 100644 index 00000000..82ce368f --- /dev/null +++ b/scripts/dependency.ps1 @@ -0,0 +1,74 @@ +# scripts/dependency.ps1 +# Usage: +# .\scripts\dependency.ps1 # install prod + dev +# .\scripts\dependency.ps1 --prod # only production +# .\scripts\dependency.ps1 --dev # only development + +param( + [switch]$prod, + [switch]$dev +) + +$ErrorActionPreference = 'Stop' + +# read same env vars as bash script, with defaults +$VENV_DIR = if ($env:VENV_DIR) { $env:VENV_DIR } else { '.venv' } +$PYPROJECT = if ($env:PYPROJECT) { $env:PYPROJECT } else { 'pyproject.toml' } +$PROD_LOCK = if ($env:PROD_LOCK) { $env:PROD_LOCK } else { 'requirements.txt' } +$DEV_LOCK = if ($env:DEV_LOCK) { $env:DEV_LOCK } else { 'requirements-dev.txt' } + +function Step($m){ Write-Host $m -ForegroundColor Yellow } +function Ok($m){ Write-Host $m -ForegroundColor Green } +function Fail($m){ Write-Host $m -ForegroundColor Red; exit 1 } + +# decide what to install +$installProd = $false +$installDev = $false +if (-not $prod -and -not $dev) { $installProd = $true; $installDev = $true } +if ($prod) { $installProd = $true } +if ($dev) { $installDev = $true } + +# activate venv +Step "โณ Activating virtualenv at '$VENV_DIR'..." +$activate = Join-Path $VENV_DIR 'Scripts\Activate.ps1' +if (-not (Test-Path $activate)) { + Fail "Virtualenv not found at '$VENV_DIR'. Create one with: python -m venv $VENV_DIR" +} +. $activate +Ok "โœ… Virtualenv activated" + +# ensure pip-tools +Step "๐Ÿ” Ensuring pip-tools is installed..." +$pipTools = & pip show pip-tools 2>$null +if (-not $pipTools) { + Step "โžก๏ธ Installing pip-tools..." + pip install pip-tools | Out-Null +} else { + Ok "โœ… pip-tools is already installed" +} + +# production +if ($installProd) { + Step "Compiling production lockfile from '$PYPROJECT'..." + pip-compile $PYPROJECT --output-file $PROD_LOCK --generate-hashes + + Step "Installing production dependencies..." + pip install --require-hashes -r $PROD_LOCK +} + +# development +if ($installDev) { + Step "Compiling development lockfile from '$PYPROJECT' extras..." + pip-compile $PYPROJECT --extra dev --output-file $DEV_LOCK --generate-hashes + + Step "Installing development dependencies..." + pip install --require-hashes -r $DEV_LOCK +} + +# verify +Step "๐Ÿ”Ž Verifying installed packages..." +pip check | Out-Host + +Ok "๐ŸŽ‰ Setup complete!" +if ($installProd) { Ok " โ€ข Production deps from $PROD_LOCK" } +if ($installDev) { Ok " โ€ข Dev deps from $DEV_LOCK" } diff --git a/scripts/env.ps1 b/scripts/env.ps1 new file mode 100644 index 00000000..d8a2d0a5 --- /dev/null +++ b/scripts/env.ps1 @@ -0,0 +1,25 @@ +# scripts/env.ps1 +$envFile = Join-Path (Get-Location) ".env" +if (Test-Path $envFile) { + Get-Content $envFile | ForEach-Object { + $line = $_.Trim() + if ($line -and -not $line.StartsWith("#")) { + $kv = $line -split "=", 2 + if ($kv.Count -eq 2) { + $key = $kv[0].Trim() + $val = $kv[1].Trim().Trim("'`"").Replace("`r","") + $val = [Environment]::ExpandEnvironmentVariables($val) + Set-Item -Path Env:$key -Value $val + } + } + } +} + +if (-not $env:PYTHONPATH) { $env:PYTHONPATH = "." } +if (-not $env:VENV_DIR) { $env:VENV_DIR = ".venv" } +if (-not $env:PROD_LOCK) { $env:PROD_LOCK = "requirements.txt" } +if (-not $env:DEV_LOCK) { $env:DEV_LOCK = "requirements-dev.txt" } +if (-not $env:PYPROJECT) { $env:PYPROJECT = "pyproject.toml" } + +$activate = Join-Path $env:VENV_DIR "Scripts\Activate.ps1" +if (Test-Path $activate) { . $activate } diff --git a/scripts/hooks/check-requirements.ps1 b/scripts/hooks/check-requirements.ps1 new file mode 100644 index 00000000..dbaafa66 --- /dev/null +++ b/scripts/hooks/check-requirements.ps1 @@ -0,0 +1,11 @@ +param([string[]]$Args) + +$changed = git diff --cached --name-only +$targets = @('requirements.txt', 'requirements-dev.txt') +$edited = $changed | Where-Object { $targets -contains $_ } + +if ($edited) { + Write-Host "requirements files changed. Use pip-compile to update lock files." -ForegroundColor Red + exit 1 +} +exit 0 diff --git a/scripts/hooks/nbstripout-install.ps1 b/scripts/hooks/nbstripout-install.ps1 new file mode 100644 index 00000000..193ab5d5 --- /dev/null +++ b/scripts/hooks/nbstripout-install.ps1 @@ -0,0 +1,14 @@ +# Installs nbstripout as a Git filter for the current repo +# Run once per repo, or call from setup.ps1 + +$ErrorActionPreference = "Stop" + +# Ensure nbstripout is present in the venv +if (-not (python -m pip show nbstripout 2>$null)) { + python -m pip install nbstripout | Out-Null +} + +# Install the filter in this repo +python -m nbstripout --install + +Write-Host "โœ… nbstripout Git filter installed for this repository." -ForegroundColor Green diff --git a/scripts/hooks/precommit_pytest.ps1 b/scripts/hooks/precommit_pytest.ps1 new file mode 100644 index 00000000..c106be24 --- /dev/null +++ b/scripts/hooks/precommit_pytest.ps1 @@ -0,0 +1,3 @@ +$env:PYTHONDONTWRITEBYTECODE = "1" +python -m pytest --cov --cov-fail-under=80 --beartype-packages=utils,example +exit $LASTEXITCODE diff --git a/scripts/new-branch.ps1 b/scripts/new-branch.ps1 new file mode 100644 index 00000000..209a7247 --- /dev/null +++ b/scripts/new-branch.ps1 @@ -0,0 +1,52 @@ +<# +Creates a new branch from origin/main and sets upstream. +Usage: + .\scripts\new-branch.ps1 feature/my-new-thing +#> + +param( + [Parameter(Position=0, Mandatory=$false)] + [string]$Name +) + +$ErrorActionPreference = "Stop" + +function Fail($m){ Write-Host "โŒ $m" -ForegroundColor Red; exit 1 } +function Step($m){ Write-Host "๐Ÿ‘‰ $m" -ForegroundColor Cyan } +function Ok($m){ Write-Host "โœ… $m" -ForegroundColor Green } + +if (-not $Name -or $Name.Trim() -eq "") { + $Name = Read-Host "Enter new branch name (e.g. feature/my-new-thing)" + if (-not $Name) { Fail "Branch name is required." } +} + +# Basic validation +if ($Name -match '^\s|[\s]$') { Fail "Branch name cannot start or end with space." } +if ($Name -match '[~^:\?\*\[\]\\ ]') { Fail "Branch name contains invalid characters." } + +# Ensure we are in a Git repo +git rev-parse --git-dir *> $null 2>&1 || Fail "Not inside a Git repository." + +Step "Fetching latest refs" +git fetch --prune + +# Ensure main exists locally +$main = "main" +if (-not (git show-ref --verify --quiet "refs/heads/$main")) { + Step "Creating local '$main' tracking branch" + git checkout -B $main "origin/$main" +} else { + Step "Checking out '$main'" + git checkout $main +} + +Step "Resetting '$main' to origin/$main" +git reset --hard "origin/$main" + +Step "Creating new branch '$Name' from origin/$main" +git checkout -B $Name "origin/$main" + +Step "Pushing and setting upstream" +git push -u origin $Name + +Ok "Branch '$Name' ready and tracking origin/$Name." diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 new file mode 100644 index 00000000..e189c894 --- /dev/null +++ b/scripts/setup.ps1 @@ -0,0 +1,67 @@ +# scripts/setup.ps1 +<# Complete Windows setup for Python Basic Template #> +Param( + [switch]$DevOnly, + [switch]$ProdOnly +) + +$ErrorActionPreference = "Stop" + +function Step($m){ Write-Host "๐Ÿ‘‰ $m" -ForegroundColor Cyan } +function Ok($m){ Write-Host "โœ… $m" -ForegroundColor Green } +function Warn($m){ Write-Host "โš  $m" -ForegroundColor Yellow } +function Fail($m){ Write-Host "โŒ $m" -ForegroundColor Red; exit 1 } + +# Load env (similar to .envrc) +. (Join-Path (Get-Location) "scripts\env.ps1") + +# 1. sanity checks +Step "Checking prerequisites" +$py = (Get-Command python -ErrorAction SilentlyContinue) +if(-not $py){ Fail "Python not found, install Python 3.12+ from python.org or winget" } +$git = (Get-Command git -ErrorAction SilentlyContinue) +if(-not $git){ Fail "git not found, install Git for Windows" } +Ok "Python: $(python --version)" +Ok "git: $(git --version)" + +# 2. venv +Step "Creating and activating virtual environment" +$venv = $env:VENV_DIR +if(Test-Path $venv){ Warn "Existing venv found, reusing" } else { python -m venv $venv } +$activate = Join-Path $venv "Scripts\Activate.ps1" +. $activate +Ok "Virtual environment activated" + +# 3. pip tooling +Step "Upgrading pip and wheel" +python -m pip install --upgrade pip wheel build + +# 4. dependencies (calls dependency.ps1) +if($DevOnly){ .\scripts\dependency.ps1 --dev } +elseif($ProdOnly){ .\scripts\dependency.ps1 --prod } +else { .\scripts\dependency.ps1 } + +# 5. developer tools +Step "Installing developer tools" +pip install pre-commit detect-secrets nbstripout pip-audit jupytext pytest pytest-cov pytest-beartype beartype + +# 6. pre-commit hooks +Step "Installing pre-commit hooks" +pre-commit install -c .pre-commit-windows.yaml +pre-commit autoupdate -c .pre-commit-windows.yaml +try { pre-commit run --all-files -c .pre-commit-windows.yaml } catch { Warn "Pre-commit found issues, fix then re-run" } + +# 7. Install nbstripout +try { + .\scripts\hooks\nbstripout-install.ps1 +} catch { + Warn "nbstripout install skipped: $($_.Exception.Message)" +} + +# 8. validation +Step "Running import smoke test" +$env:PYTHONPATH = (Get-Location).Path +python -c "import utils.secrets; print('โœ… All project modules import successfully')" + +Ok "๐ŸŽ‰ Windows setup complete" + From a055087e118277afdd816655cf39b7af89ddde52 Mon Sep 17 00:00:00 2001 From: divaanster Date: Thu, 28 Aug 2025 12:01:09 +0200 Subject: [PATCH 036/199] minor change to setup.ps1 to properly install utils.secrets --- scripts/setup.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index e189c894..dddf5bb4 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -45,6 +45,10 @@ else { .\scripts\dependency.ps1 } Step "Installing developer tools" pip install pre-commit detect-secrets nbstripout pip-audit jupytext pytest pytest-cov pytest-beartype beartype +# 6. Installing project +Step "Installing project in editable mode" +pip install -e . + # 6. pre-commit hooks Step "Installing pre-commit hooks" pre-commit install -c .pre-commit-windows.yaml From df07fc74552e3ece86712ccae7a6cad16c464c04 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 13 Aug 2025 15:58:31 +0200 Subject: [PATCH 037/199] test automatically fixed code (rust) commit --- .pre-commit-config.yaml | 2 +- main.py | 9 +++ pyproject.toml | 2 + requirements-dev.txt | 4 +- requirements.txt | 173 +++++++++++++++++++--------------------- scripts/dependency.sh | 8 +- scripts/setup.sh | 6 ++ 7 files changed, 108 insertions(+), 96 deletions(-) create mode 100644 main.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d0ee4588..e98af86a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.12.8 hooks: - id: ruff name: Ruff (linting + extra isort + rules) diff --git a/main.py b/main.py new file mode 100644 index 00000000..ea091eab --- /dev/null +++ b/main.py @@ -0,0 +1,9 @@ +# main.py + +from utils.secrets import get_secret + +API_KEY = get_secret("op://Vaanster/Vaanster ZeroFriction Standard API/credential") + + +if __name__ == "__main__": + print(f"testing secret retrieval: {API_KEY}") # Bad indentation diff --git a/pyproject.toml b/pyproject.toml index e445ede1..9ab94712 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ # Add project dependencies here. pip-tools will read them from here, do not use a requirements file! dependencies = [ "flask>=2.2.0,<3.0.0", # Example dependency, replace with your own + "requests" ] [project.optional-dependencies] @@ -43,6 +44,7 @@ dev = [ "pytest-beartype", "pip-audit>=2.9.0", "pip-tools", # for managing requirement*.txt + "rich", ] [project.urls] diff --git a/requirements-dev.txt b/requirements-dev.txt index fce726f7..9fd5ddd8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -677,7 +677,9 @@ requests==2.32.4 \ rich==14.1.0 \ --hash=sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f \ --hash=sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8 - # via pip-audit + # via + # pip-audit + # python-basic-template (pyproject.toml) rpds-py==0.26.0 \ --hash=sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3 \ --hash=sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b \ diff --git a/requirements.txt b/requirements.txt index d1165559..d2be873c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,99 +12,86 @@ certifi==2025.8.3 \ --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 # via requests -charset-normalizer==3.4.2 \ - --hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \ - --hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \ - --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \ - --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \ - --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \ - --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \ - --hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \ - --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \ - --hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \ - --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \ - --hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \ - --hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \ - --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \ - --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \ - --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \ - --hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \ - --hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \ - --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \ - --hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \ - --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \ - --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \ - --hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \ - --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \ - --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \ - --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \ - --hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \ - --hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \ - --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \ - --hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \ - --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \ - --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \ - --hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \ - --hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \ - --hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \ - --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \ - --hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \ - --hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \ - --hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \ - --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \ - --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \ - --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \ - --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \ - --hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \ - --hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \ - --hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \ - --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \ - --hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \ - --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \ - --hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \ - --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \ - --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \ - --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \ - --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \ - --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \ - --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \ - --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \ - --hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \ - --hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \ - --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \ - --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \ - --hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \ - --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \ - --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \ - --hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \ - --hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \ - --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \ - --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \ - --hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \ - --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \ - --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \ - --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \ - --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \ - --hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \ - --hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \ - --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \ - --hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \ - --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \ - --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \ - --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \ - --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \ - --hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \ - --hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \ - --hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \ - --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \ - --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \ - --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \ - --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \ - --hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \ - --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \ - --hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \ - --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \ - --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f +charset-normalizer==3.4.3 \ + --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ + --hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \ + --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ + --hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \ + --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ + --hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \ + --hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \ + --hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \ + --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ + --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ + --hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \ + --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ + --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ + --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ + --hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \ + --hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \ + --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ + --hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \ + --hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \ + --hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \ + --hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \ + --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ + --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ + --hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \ + --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ + --hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \ + --hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \ + --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ + --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ + --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ + --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ + --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ + --hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \ + --hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \ + --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ + --hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \ + --hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \ + --hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \ + --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ + --hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \ + --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ + --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ + --hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \ + --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ + --hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \ + --hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \ + --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ + --hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \ + --hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \ + --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ + --hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \ + --hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \ + --hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \ + --hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \ + --hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \ + --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ + --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ + --hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \ + --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ + --hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \ + --hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \ + --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ + --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ + --hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \ + --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ + --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ + --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ + --hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \ + --hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \ + --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ + --hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \ + --hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \ + --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ + --hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \ + --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ + --hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \ + --hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \ + --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ + --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 # via requests click==8.2.1 \ --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ diff --git a/scripts/dependency.sh b/scripts/dependency.sh index 91e9b47a..a2311bbb 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -1,6 +1,12 @@ #!/usr/bin/env bash set -euo pipefail +# Configuration +export VENV_DIR="${VENV_DIR:-venv}" +export PYPROJECT="${PYPROJECT:-pyproject.toml}" +export PROD_LOCK="${PROD_LOCK:-requirements.txt}" +export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" + # Hybrid dependency management script: # Usage: # ./dependency.sh # install both prod & dev @@ -81,4 +87,4 @@ pip check echo -e "${GREEN}๐ŸŽ‰ Setup complete!${RESET}" [ "$INSTALL_PROD" = true ] && echo -e "${GREEN} โ€ข Production deps from ${PROD_LOCK}${RESET}" -[ "$INSTALL_DEV" = true ] && echo -e "${GREEN} โ€ข Dev deps from ${DEV_LOCK}${RESET}" \ No newline at end of file +[ "$INSTALL_DEV" = true ] && echo -e "${GREEN} โ€ข Dev deps from ${DEV_LOCK}${RESET}" diff --git a/scripts/setup.sh b/scripts/setup.sh index 9f170421..52ae0e0c 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,6 +1,12 @@ #!/usr/bin/env bash set -euo pipefail +# configuration +export VENV_DIR="venv" +export PYPROJECT="pyproject.toml" +export PROD_LOCK="requirements.txt" +export DEV_LOCK="requirements-dev.txt" + # Complete development environment setup script for Python Basic Template # This script sets up everything a developer needs to start working on the project: # - Virtual environment From 2c31400379a8572ac9bde1ea900e2d27cf847d9b Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 13 Aug 2025 16:14:30 +0200 Subject: [PATCH 038/199] test pre commit hooks: add fake secret --- main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.py b/main.py index ea091eab..d6b6e3fc 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,8 @@ API_KEY = get_secret("op://Vaanster/Vaanster ZeroFriction Standard API/credential") +# TODO: remove this test key +FAKE_AWS_KEY = "AKIAOSFODNN7EXAMPLE" if __name__ == "__main__": print(f"testing secret retrieval: {API_KEY}") # Bad indentation From 200c05f450549eeaa601bb7b35c936d68cb82b9d Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 14 Aug 2025 15:02:14 +0200 Subject: [PATCH 039/199] add manual edit error handling prompt for user --- .pre-commit-config.yaml | 20 +++++-- .secrets.baseline | 27 +++++++++- main.py | 2 - pyproject.toml | 1 + scripts/dependency.sh | 106 ++++++++++++++++++++++++++++--------- scripts/verify-lockfile.sh | 38 +++++++++++++ 6 files changed, 161 insertions(+), 33 deletions(-) create mode 100755 scripts/verify-lockfile.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e98af86a..98656e27 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,11 +2,21 @@ repos: - repo: local hooks: # Prevent manual requirements.txt files - - id: prevent-manual-requirements-edits - name: Prevent manual changes to requirements.txt - entry: scripts/hooks/check-requirements.sh - language: script - types: [text] + - id: verify-prod-lockfile + name: Verify production lockfile is up-to-date + entry: ./scripts/verify-lockfile.sh + args: ["requirements.txt"] + language: python + files: ^requirements\.txt$ + additional_dependencies: [pip-tools] + + - id: verify-dev-lockfile + name: Verify development lockfile is up-to-date + entry: ./scripts/verify-lockfile.sh + args: ["requirements-dev.txt"] + language: python + files: ^requirements-dev\.txt$ + additional_dependencies: [pip-tools] # Strip notebook outputs - id: nbstripout-autoadd diff --git a/.secrets.baseline b/.secrets.baseline index ad4e42b3..d05b2c0c 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -90,6 +90,10 @@ { "path": "detect_secrets.filters.allowlist.is_line_allowlisted" }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "min_level": 2 @@ -122,6 +126,25 @@ "path": "detect_secrets.filters.heuristic.is_templated_secret" } ], - "results": {}, - "generated_at": "2025-08-05T11:33:54Z" + "results": { + "tests/utils/test_secrets.py": [ + { + "type": "Secret Keyword", + "filename": "tests/utils/test_secrets.py", + "hashed_secret": "a761ce3a45d97e41840a788495e85a70d1bb3815", + "is_verified": false, + "line_number": 26, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "tests/utils/test_secrets.py", + "hashed_secret": "d203e7b3428caf7fd12b7f15410cb0108d6c041b", + "is_verified": false, + "line_number": 41, + "is_secret": false + } + ] + }, + "generated_at": "2025-08-13T14:21:51Z" } diff --git a/main.py b/main.py index d6b6e3fc..ea091eab 100644 --- a/main.py +++ b/main.py @@ -4,8 +4,6 @@ API_KEY = get_secret("op://Vaanster/Vaanster ZeroFriction Standard API/credential") -# TODO: remove this test key -FAKE_AWS_KEY = "AKIAOSFODNN7EXAMPLE" if __name__ == "__main__": print(f"testing secret retrieval: {API_KEY}") # Bad indentation diff --git a/pyproject.toml b/pyproject.toml index 9ab94712..ed69e1f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,6 +128,7 @@ line-length = 88 fix = true respect-gitignore = true extend-exclude = ["migrations"] +dummy-variable-rgx = "^_|^__|^pytest$" # Enable popular rule sets lint.select = [ diff --git a/scripts/dependency.sh b/scripts/dependency.sh index a2311bbb..ac1a1492 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -1,27 +1,49 @@ #!/usr/bin/env bash set -euo pipefail +# ----------------------------------------------------------------------------- # Configuration +# ----------------------------------------------------------------------------- +# Use environment variables if they exist, otherwise set sensible defaults. export VENV_DIR="${VENV_DIR:-venv}" export PYPROJECT="${PYPROJECT:-pyproject.toml}" export PROD_LOCK="${PROD_LOCK:-requirements.txt}" export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" +# ----------------------------------------------------------------------------- +# Script Info +# ----------------------------------------------------------------------------- # Hybrid dependency management script: +# Compiles lockfiles first, then installs, to ensure a robust workflow. +# # Usage: -# ./dependency.sh # install both prod & dev -# ./dependency.sh --prod # install only production deps -# ./dependency.sh --dev # install only development deps +# ./scripts/dependency.sh # Compile and install both prod & dev +# ./scripts/dependency.sh --prod # Compile and install only prod +# ./scripts/dependency.sh --dev # Compile and install only dev +# ----------------------------------------------------------------------------- +# UI Functions +# ----------------------------------------------------------------------------- # Colors YELLOW='\033[1;33m' GREEN='\033[1;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' RESET='\033[0m' -# Parse args +# Helper functions for printing formatted output +info() { echo -e "${BLUE}โ„น๏ธ $1${RESET}"; } +step() { echo -e "\n${YELLOW}$1${RESET}"; } +success() { echo -e "${GREEN}โœ… $1${RESET}"; } +error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } + +# ----------------------------------------------------------------------------- +# Argument Parsing +# ----------------------------------------------------------------------------- INSTALL_PROD=false INSTALL_DEV=false +# Default to both if no arguments are provided if [ $# -eq 0 ]; then INSTALL_PROD=true INSTALL_DEV=true @@ -35,56 +57,92 @@ else INSTALL_DEV=true ;; *) - echo "โŒ Unknown option: $arg" - echo "Usage: $0 [--prod] [--dev]" - exit 1 + error "Unknown option: $arg. Usage: $0 [--prod] [--dev]" ;; esac done fi -echo -e "${YELLOW}โณ Activating virtualenv at '${VENV_DIR}'...${RESET}" +# ----------------------------------------------------------------------------- +# Environment Setup +# ----------------------------------------------------------------------------- +step "Activating virtualenv at '${VENV_DIR}'..." if [ -f "${VENV_DIR}/bin/activate" ]; then # shellcheck disable=SC1090 source "${VENV_DIR}/bin/activate" + success "Virtual environment activated." else - echo "โŒ Virtualenv not found at '${VENV_DIR}'." - echo " Create one with: python -m venv ${VENV_DIR}" - exit 1 + error "Virtualenv not found at '${VENV_DIR}'. Please run setup script first." fi -echo -e "${YELLOW}๐Ÿ” Ensuring pip-tools is installed...${RESET}" +step "Ensuring pip-tools is installed..." if ! pip show pip-tools &>/dev/null; then - echo -e "${YELLOW}โžก๏ธ Installing pip-tools...${RESET}" + info "Installing pip-tools..." pip install pip-tools + success "pip-tools installed." else - echo -e "${GREEN}โœ… pip-tools is already installed.${RESET}" + success "pip-tools is already installed." fi +# ============================================================================= +# MAJOR CHANGE: The script is now split into a COMPILE phase and an INSTALL phase. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# PHASE 1: COMPILE ALL REQUIRED LOCKFILES +# ----------------------------------------------------------------------------- +# This block runs first to ensure our lockfiles are valid before we try to +# install from them. This prevents errors if a file was manually edited. + +step "PHASE 1: Compiling lockfiles from '${PYPROJECT}'..." + if [ "$INSTALL_PROD" = true ]; then - echo -e "${YELLOW}๐Ÿงฎ Compiling production lockfile from '${PYPROJECT}'...${RESET}" + info "Compiling production lockfile: '${PROD_LOCK}'" pip-compile "${PYPROJECT}" \ --output-file="${PROD_LOCK}" \ --generate-hashes - - echo -e "${YELLOW}๐Ÿ“ฅ Installing production dependencies...${RESET}" - pip install --require-hashes -r "${PROD_LOCK}" fi if [ "$INSTALL_DEV" = true ]; then - echo -e "${YELLOW}๐Ÿ›  Compiling development lockfile from '${PYPROJECT}' extras...${RESET}" + info "Compiling development lockfile: '${DEV_LOCK}'" pip-compile "${PYPROJECT}" \ --extra=dev \ --output-file="${DEV_LOCK}" \ --generate-hashes +fi + +success "All required lockfiles compiled." - echo -e "${YELLOW}๐Ÿ“ฅ Installing development dependencies...${RESET}" +# ----------------------------------------------------------------------------- +# PHASE 2: INSTALL DEPENDENCIES FROM THE (NOW VERIFIED) LOCKFILES +# ----------------------------------------------------------------------------- +# Now that we know the lockfiles are correct, we can safely install from them. + +step "PHASE 2: Installing dependencies..." + +if [ "$INSTALL_PROD" = true ]; then + info "Installing production dependencies from '${PROD_LOCK}'..." + # We use --require-hashes for a secure install. + pip install --require-hashes -r "${PROD_LOCK}" +fi + +if [ "$INSTALL_DEV" = true ]; then + info "Installing development dependencies from '${DEV_LOCK}'..." + # The dev requirements should include the prod ones, but `pip install` handles duplicates gracefully. pip install --require-hashes -r "${DEV_LOCK}" fi -echo -e "${YELLOW}๐Ÿ”Ž Verifying installed packages...${RESET}" +success "All required dependencies installed." + +# ----------------------------------------------------------------------------- +# Final Verification +# ----------------------------------------------------------------------------- +step "Verifying environment consistency..." pip check +success "Environment check passed." -echo -e "${GREEN}๐ŸŽ‰ Setup complete!${RESET}" -[ "$INSTALL_PROD" = true ] && echo -e "${GREEN} โ€ข Production deps from ${PROD_LOCK}${RESET}" -[ "$INSTALL_DEV" = true ] && echo -e "${GREEN} โ€ข Dev deps from ${DEV_LOCK}${RESET}" +# --- Final Summary --- +echo +success "Dependency setup complete!" +[ "$INSTALL_PROD" = true ] && echo " โ€ข Production dependencies updated from '${PROD_LOCK}'" +[ "$INSTALL_DEV" = true ] && echo " โ€ข Development dependencies updated from '${DEV_LOCK}'" diff --git a/scripts/verify-lockfile.sh b/scripts/verify-lockfile.sh new file mode 100755 index 00000000..38616028 --- /dev/null +++ b/scripts/verify-lockfile.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script verifies that a lockfile is up-to-date with pyproject.toml. +# It's designed to be called by a pre-commit hook. +# It accepts one argument: the path to the lockfile to check (e.g., requirements.txt) + +# --- Configuration --- +LOCKFILE_TO_CHECK="$1" +PYPROJECT_FILE="pyproject.toml" +TEMP_LOCKFILE=$(mktemp) # Creates a secure temporary file + +# --- Logic --- +echo "Verifying integrity of '$LOCKFILE_TO_CHECK'..." + +# Determine which compile command to run based on the filename +if [[ "$LOCKFILE_TO_CHECK" == "requirements-dev.txt" ]]; then + # Compile development dependencies + pip-compile "$PYPROJECT_FILE" --extra=dev --output-file="$TEMP_LOCKFILE" > /dev/null +else + # Compile production dependencies by default + pip-compile "$PYPROJECT_FILE" --output-file="$TEMP_LOCKFILE" > /dev/null +fi + +# Compare the officially generated temp file with the one being committed. +# The `diff -q` command is quiet and exits with a non-zero status if files differ. +if ! diff -q "$LOCKFILE_TO_CHECK" "$TEMP_LOCKFILE"; then + echo "โŒ Error: '$LOCKFILE_TO_CHECK' has been manually edited or is out of date." >&2 + echo " It does not match the expected output based on '$PYPROJECT_FILE'." >&2 + echo " To fix this, run 'rm requirements.txt' and then './scripts/dependency.sh' and commit the resulting changes." >&2 + rm "$TEMP_LOCKFILE" # Always clean up + exit 1 +fi + +# If diff exits with 0, the files are identical. +echo "โœ… '$LOCKFILE_TO_CHECK' is valid and up-to-date." +rm "$TEMP_LOCKFILE" # Always clean up +exit 0 From 878d325269da28d906c419d8f948a5a0d2c3adc9 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 14 Aug 2025 16:19:30 +0200 Subject: [PATCH 040/199] delete testing files not necessary for template --- main.py | 9 -- pyproject.toml | 2 +- scripts/{setup.sh => setup-linux.sh} | 0 scripts/setup-wsl.sh | 207 +++++++++++++++++++++++++++ setup.py | 8 -- 5 files changed, 208 insertions(+), 18 deletions(-) delete mode 100644 main.py rename scripts/{setup.sh => setup-linux.sh} (100%) create mode 100644 scripts/setup-wsl.sh delete mode 100644 setup.py diff --git a/main.py b/main.py deleted file mode 100644 index ea091eab..00000000 --- a/main.py +++ /dev/null @@ -1,9 +0,0 @@ -# main.py - -from utils.secrets import get_secret - -API_KEY = get_secret("op://Vaanster/Vaanster ZeroFriction Standard API/credential") - - -if __name__ == "__main__": - print(f"testing secret retrieval: {API_KEY}") # Bad indentation diff --git a/pyproject.toml b/pyproject.toml index ed69e1f0..c68a544d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,7 +128,7 @@ line-length = 88 fix = true respect-gitignore = true extend-exclude = ["migrations"] -dummy-variable-rgx = "^_|^__|^pytest$" +lint.dummy-variable-rgx = "^_|^__|^pytest$" # Enable popular rule sets lint.select = [ diff --git a/scripts/setup.sh b/scripts/setup-linux.sh similarity index 100% rename from scripts/setup.sh rename to scripts/setup-linux.sh diff --git a/scripts/setup-wsl.sh b/scripts/setup-wsl.sh new file mode 100644 index 00000000..9f170421 --- /dev/null +++ b/scripts/setup-wsl.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Complete development environment setup script for Python Basic Template +# This script sets up everything a developer needs to start working on the project: +# - Virtual environment +# - Hybrid dependency management (production + development) +# - Pre-commit hooks +# - Secret management with 1Password +# - Development tools configuration + +# Colors +GREEN='\033[0;32m' +CYAN='\033[1;36m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +RESET='\033[0m' + +step() { echo -e "${CYAN}๐Ÿ‘‰ $1${RESET}"; } +success(){ echo -e "${GREEN}โœ… $1${RESET}"; } +warn() { echo -e "${YELLOW}โš ๏ธ $1${RESET}"; } +error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } +info() { echo -e "${BLUE}โ„น๏ธ $1${RESET}"; } +section(){ + echo -e "\n${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" + echo -e "${BLUE}๐Ÿ”ง $1${RESET}" + echo -e "${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" +} + +setup_development_environment() { + section "SYSTEM REQUIREMENTS CHECK" + step "Verifying required system packages..." + info "Checking for Python 3..." + command -v python3 >/dev/null || error "Python 3 is not installed. Please install Python 3.12+." + PYTHON_VERSION=$(python3 --version | cut -d' ' -f2) + success "Python $PYTHON_VERSION found." + + info "Checking for pip..." + command -v pip >/dev/null || error "pip is not installed. Please install pip." + PIP_VERSION=$(pip --version | cut -d' ' -f2) + success "pip $PIP_VERSION found." + + info "Checking for curl..." + command -v curl >/dev/null || error "curl is not installed. Please install curl." + success "curl is available." + + info "Checking for git..." + command -v git >/dev/null || error "git is not installed. Please install git." + GIT_VERSION=$(git --version | cut -d' ' -f3) + success "git $GIT_VERSION found." + + section "VIRTUAL ENVIRONMENT SETUP" + step "Creating Python virtual environment..." + if [ -d "$VENV_DIR" ]; then + warn "Virtual environment already exists." + echo -e "${YELLOW}Choose an option:${RESET}" + echo " 1) Skip creating a new virtual environment (keep existing)" + echo " 2) Remove old one and create a new virtual environment" + read -p "Enter 1 or 2 [default: 2]: " VENV_CHOICE + VENV_CHOICE=${VENV_CHOICE:-2} + if [ "$VENV_CHOICE" = "1" ]; then + success "Keeping existing virtual environment." + source $VENV_DIR/bin/activate + elif [ "$VENV_CHOICE" = "2" ]; then + warn "Removing old virtual environment..." + rm -rf $VENV_DIR + python3 -m venv $VENV_DIR + source $VENV_DIR/bin/activate + success "Virtual environment created and activated." + else + error "Invalid choice. Exiting." + fi + else + python3 -m venv $VENV_DIR + source $VENV_DIR/bin/activate + success "Virtual environment created and activated." + fi + + step "Upgrading pip in virtual environment..." + pip install --upgrade pip + PIP_NEW_VERSION=$(pip --version | cut -d' ' -f2) + success "pip upgraded to version $PIP_NEW_VERSION." + + section "HYBRID DEPENDENCY MANAGEMENT" + info "This project uses a hybrid dependency approach:" + info " โ€ข Production deps : declared in $PYPROJECT โ†’ locked to $PROD_LOCK" + info " โ€ข Development deps : declared in $PYPROJECT [project.optional-dependencies.dev] โ†’ locked to $DEV_LOCK" + + step "Installing dependency management tools (pip-tools, build, wheel)..." + pip install pip-tools build wheel + success "pip-tools, build, and wheel installed." + + step "Running dependency setup script (compiles & installs deps)..." + ./scripts/dependency.sh + success "Production and development dependencies installed." + + section "SYSTEM LOCALE CONFIGURATION" + step "Updating system locales..." + sudo apt update + sudo apt install -y locales + sudo locale-gen en_US.UTF-8 + sudo update-locale LANG=en_US.UTF-8 + success "System locales configured." + + section "DIRENV SETUP" + step "Installing direnv for environment variable management..." + sudo apt install -y direnv + success "direnv installed." + step "Configuring direnv shell integration..." + PROFILE="${HOME}/.bashrc" + if ! grep -q 'direnv hook bash' "$PROFILE"; then + echo 'eval "$(direnv hook bash)"' >> "$PROFILE" + success "Added direnv hook to $PROFILE." + else + warn "direnv hook already present in $PROFILE." + fi + success "Shell configuration reloaded." + step "Enabling direnv for this project..." + if [ -f ".envrc" ]; then + direnv allow + success "direnv enabled for this project." + else + warn ".envrc file not found. direnv configuration may be incomplete." + fi + + section "PRE-COMMIT HOOKS SETUP" + step "Installing pre-commit hooks..." + if [[ -f ".pre-commit-config.yaml" ]]; then + pre-commit install + success "Pre-commit hooks installed." + step "Updating pre-commit hook repositories..." + pre-commit autoupdate + success "Pre-commit hooks updated." + step "Running pre-commit on all files (initial check)..." + pre-commit run --all-files || warn "Some pre-commit checks failed (initial run)." + info "Pre-commit will now run automatically on commits." + else + warn ".pre-commit-config.yaml not found. Skipping hooks install." + fi + + section "SECRET MANAGEMENT SETUP" + step "Checking 1Password CLI integration..." + if op --version &>/dev/null; then + success "1Password CLI is installed." + step "Testing 1Password connection..." + if op account list &>/dev/null; then + success "1Password CLI is connected." + else + warn "1Password CLI not signed in." + fi + else + error "1Password CLI not found. Install & configure before secrets workflow." + exit 1 + fi + step "Initializing secrets baseline with detect-secrets..." + if [ -f ".secrets.baseline" ]; then + warn "Existing .secrets.baseline found; backing up." + cp .secrets.baseline .secrets.baseline.bak + fi + detect-secrets scan > .secrets.baseline + success "Secrets baseline created." + detect-secrets audit .secrets.baseline + success "Secrets configuration verified." + + section "DEVELOPMENT TOOLS VERIFICATION" + step "Verifying development tools..." + for tool in black ruff mypy pytest beartype; do + info "Checking $tool..." + "pip show $tool" &>/dev/null && success "$tool is installed." || warn "$tool missing." + done + + section "PROJECT VALIDATION" + step "Running final project validation..." + info "Checking imports..." + python - <<'PYCODE' +import utils.secrets, utils.utils +print("โœ… All project modules import successfully") +PYCODE + info "Syntax-checking all Python files..." + find . -name "*.py" -not -path "./$VENV_DIR/*" -exec python -m py_compile {} \; + success "Python syntax valid." + if [ -s "$PROD_LOCK" ]; then + info "Verifying production dependencies..." + pip check + success "Dependencies compatible." + else + info "No prod dependencies to verify." + fi + + section "SETUP COMPLETE" + echo -e "\n${GREEN}๐ŸŽ‰ DEVELOPMENT ENVIRONMENT SETUP COMPLETE!${RESET}\n" + echo -e "${BLUE}Installed:${RESET}" + echo " โ€ข $VENV_DIR/" + echo " โ€ข Production deps ($PROD_LOCK)" + echo " โ€ข Dev tools ($DEV_LOCK)" + echo " โ€ข Pre-commit hooks" + echo " โ€ข Secret baseline (.secrets.baseline)" + echo " โ€ข direnv" + echo -e "\n${BLUE}Next steps:${RESET}" + echo " 1. Restart terminal or run: source ~/.bashrc" + echo " 2. Test with: python example/using_secrets.py" + echo " 3. Start coding!" +} + +# Main +setup_development_environment diff --git a/setup.py b/setup.py deleted file mode 100644 index 9084afae..00000000 --- a/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -# setup.py is meant for compatibility with older Python packaging tools. -# It is not strictly necessary if you are using PEP 517/518 compliant build tools. -# However, it can be useful for certain environments or legacy systems. -# Do not remove or modify this file unless you are sure you do not need it. -from setuptools import setup - -if __name__ == "__main__": - setup() From c01ee0cdc2fe5561e8ead0eeea9fc0a7215f7a36 Mon Sep 17 00:00:00 2001 From: Dominic Istha Date: Wed, 10 Sep 2025 13:46:09 +0200 Subject: [PATCH 041/199] changes ruff version to v0.12.8 --- .pre-commit-config.yaml | 2 +- .pre-commit-windows.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d0ee4588..e98af86a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.12.8 hooks: - id: ruff name: Ruff (linting + extra isort + rules) diff --git a/.pre-commit-windows.yaml b/.pre-commit-windows.yaml index 66a8086f..f06bbdf6 100644 --- a/.pre-commit-windows.yaml +++ b/.pre-commit-windows.yaml @@ -45,7 +45,7 @@ repos: args: ["--config-file=pyproject.toml"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.10 + rev: v0.12.8 hooks: - id: ruff args: [--fix] From 8076dc256473522407361229c0d589be2357313e Mon Sep 17 00:00:00 2001 From: Dominic Istha Date: Wed, 10 Sep 2025 13:51:36 +0200 Subject: [PATCH 042/199] copies verify-lockfile.sh to scripts/hooks --- scripts/hooks/verify-lockfile.sh | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 scripts/hooks/verify-lockfile.sh diff --git a/scripts/hooks/verify-lockfile.sh b/scripts/hooks/verify-lockfile.sh new file mode 100644 index 00000000..38616028 --- /dev/null +++ b/scripts/hooks/verify-lockfile.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script verifies that a lockfile is up-to-date with pyproject.toml. +# It's designed to be called by a pre-commit hook. +# It accepts one argument: the path to the lockfile to check (e.g., requirements.txt) + +# --- Configuration --- +LOCKFILE_TO_CHECK="$1" +PYPROJECT_FILE="pyproject.toml" +TEMP_LOCKFILE=$(mktemp) # Creates a secure temporary file + +# --- Logic --- +echo "Verifying integrity of '$LOCKFILE_TO_CHECK'..." + +# Determine which compile command to run based on the filename +if [[ "$LOCKFILE_TO_CHECK" == "requirements-dev.txt" ]]; then + # Compile development dependencies + pip-compile "$PYPROJECT_FILE" --extra=dev --output-file="$TEMP_LOCKFILE" > /dev/null +else + # Compile production dependencies by default + pip-compile "$PYPROJECT_FILE" --output-file="$TEMP_LOCKFILE" > /dev/null +fi + +# Compare the officially generated temp file with the one being committed. +# The `diff -q` command is quiet and exits with a non-zero status if files differ. +if ! diff -q "$LOCKFILE_TO_CHECK" "$TEMP_LOCKFILE"; then + echo "โŒ Error: '$LOCKFILE_TO_CHECK' has been manually edited or is out of date." >&2 + echo " It does not match the expected output based on '$PYPROJECT_FILE'." >&2 + echo " To fix this, run 'rm requirements.txt' and then './scripts/dependency.sh' and commit the resulting changes." >&2 + rm "$TEMP_LOCKFILE" # Always clean up + exit 1 +fi + +# If diff exits with 0, the files are identical. +echo "โœ… '$LOCKFILE_TO_CHECK' is valid and up-to-date." +rm "$TEMP_LOCKFILE" # Always clean up +exit 0 From 4bcdf36e531f2d52ab67530e0f49da25c9e15774 Mon Sep 17 00:00:00 2001 From: Dominic Istha Date: Wed, 10 Sep 2025 13:55:16 +0200 Subject: [PATCH 043/199] adds verify-lockfile.ps1 --- scripts/hooks/verify-lockfile.ps1 | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 scripts/hooks/verify-lockfile.ps1 diff --git a/scripts/hooks/verify-lockfile.ps1 b/scripts/hooks/verify-lockfile.ps1 new file mode 100644 index 00000000..52fc7e54 --- /dev/null +++ b/scripts/hooks/verify-lockfile.ps1 @@ -0,0 +1,43 @@ +Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Lockfile, + + [string]$PyProject = "pyproject.toml" +) + +$ErrorActionPreference = "Stop" + +function Step($m){ Write-Host "๐Ÿ‘‰ $m" -ForegroundColor Cyan } +function Ok($m){ Write-Host "โœ… $m" -ForegroundColor Green } +function Fail($m){ Write-Host "โŒ $m" -ForegroundColor Red; exit 1 } + +if (-not (Test-Path $PyProject)) { Fail "Cannot find $PyProject in repo root" } +if (-not (Test-Path $Lockfile)) { Fail "Cannot find $Lockfile in repo root" } + +$python = (Get-Command python -ErrorAction SilentlyContinue) +if (-not $python) { Fail "Python not found, install Python 3.12+ and ensure it is on PATH" } + +$temp = [System.IO.Path]::GetTempFileName() +try { + Step "Verifying integrity of '$Lockfile'" + + $isDev = ($Lockfile -like "*requirements-dev.txt") + + $args = @("piptools", "compile", $PyProject, "--output-file", $temp) + if ($isDev) { $args += @("--extra", "dev") } + + python -m $args | Out-Null + + $hashA = Get-FileHash -Algorithm SHA256 -Path $Lockfile + $hashB = Get-FileHash -Algorithm SHA256 -Path $temp + + if ($hashA.Hash -ne $hashB.Hash) { + Fail ("'$Lockfile' is out of date. Remove it and run '.\scripts\dependency.ps1', then commit the regenerated file.") + } + + Ok "'$Lockfile' is valid and up to date" +} +finally { + if (Test-Path $temp) { Remove-Item $temp -Force -ErrorAction SilentlyContinue } +} From 45de203f6769c4bcb0c9bd2109868ce4e2c4e25b Mon Sep 17 00:00:00 2001 From: Dominic Istha Date: Wed, 10 Sep 2025 13:57:04 +0200 Subject: [PATCH 044/199] Adds verify-prod-lockfile and verify-dev-lockfile for linux and windows --- scripts/hooks/verify-dev-lockfile.ps1 | 3 +++ scripts/hooks/verify-dev-lockfile.sh | 3 +++ scripts/hooks/verify-prod-lockfile.ps1 | 3 +++ scripts/hooks/verify-prod-lockfile.sh | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 scripts/hooks/verify-dev-lockfile.ps1 create mode 100644 scripts/hooks/verify-dev-lockfile.sh create mode 100644 scripts/hooks/verify-prod-lockfile.ps1 create mode 100644 scripts/hooks/verify-prod-lockfile.sh diff --git a/scripts/hooks/verify-dev-lockfile.ps1 b/scripts/hooks/verify-dev-lockfile.ps1 new file mode 100644 index 00000000..fc745135 --- /dev/null +++ b/scripts/hooks/verify-dev-lockfile.ps1 @@ -0,0 +1,3 @@ +# Wrapper for requirements-dev.txt +$ErrorActionPreference = "Stop" +& "$PSScriptRoot\verify-lockfile.ps1" -Lockfile "requirements-dev.txt" diff --git a/scripts/hooks/verify-dev-lockfile.sh b/scripts/hooks/verify-dev-lockfile.sh new file mode 100644 index 00000000..c6ce08e1 --- /dev/null +++ b/scripts/hooks/verify-dev-lockfile.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +"$(dirname "$0")/verify-lockfile.sh" requirements-dev.txt diff --git a/scripts/hooks/verify-prod-lockfile.ps1 b/scripts/hooks/verify-prod-lockfile.ps1 new file mode 100644 index 00000000..ba48b932 --- /dev/null +++ b/scripts/hooks/verify-prod-lockfile.ps1 @@ -0,0 +1,3 @@ +# Wrapper for requirements.txt +$ErrorActionPreference = "Stop" +& "$PSScriptRoot\verify-lockfile.ps1" -Lockfile "requirements.txt" diff --git a/scripts/hooks/verify-prod-lockfile.sh b/scripts/hooks/verify-prod-lockfile.sh new file mode 100644 index 00000000..2107b477 --- /dev/null +++ b/scripts/hooks/verify-prod-lockfile.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +"$(dirname "$0")/verify-lockfile.sh" requirements.txt From 859d7a39683538f36d02a9748a64644041b882e4 Mon Sep 17 00:00:00 2001 From: Dominic Istha Date: Wed, 10 Sep 2025 14:01:17 +0200 Subject: [PATCH 045/199] updates pre-commit config.yaml files to include verify-prod-lockfile and verify-dev-lockfile --- .pre-commit-config.yaml | 20 ++++++++++++++------ .pre-commit-windows.yaml | 16 ++++++++++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e98af86a..dc6d9a02 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,20 @@ repos: - repo: local hooks: - # Prevent manual requirements.txt files - - id: prevent-manual-requirements-edits - name: Prevent manual changes to requirements.txt - entry: scripts/hooks/check-requirements.sh - language: script - types: [text] + # Verify requirements lockfiles + - id: verify-prod-lockfile + name: Verify prod lockfile (Linux) + entry: ./scripts/hooks/verify-prod-lockfile.sh + language: system + pass_filenames: false + stages: [pre-commit] + + - id: verify-dev-lockfile + name: Verify dev lockfile (Linux) + entry: ./scripts/hooks/verify-dev-lockfile.sh + language: system + pass_filenames: false + stages: [pre-commit] # Strip notebook outputs - id: nbstripout-autoadd diff --git a/.pre-commit-windows.yaml b/.pre-commit-windows.yaml index f06bbdf6..65d94888 100644 --- a/.pre-commit-windows.yaml +++ b/.pre-commit-windows.yaml @@ -1,11 +1,19 @@ repos: - repo: local hooks: - - id: prevent-manual-requirements-edits - name: Prevent manual changes to requirements.txt - entry: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\hooks\\check-requirements.ps1 + - id: verify-prod-lockfile + name: Verify prod lockfile (Windows) + entry: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\hooks\\verify-prod-lockfile.ps1 language: system - types: [text] + pass_filenames: false + stages: [pre-commit] + + - id: verify-dev-lockfile + name: Verify dev lockfile (Windows) + entry: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\hooks\\verify-dev-lockfile.ps1 + language: system + pass_filenames: false + stages: [pre-commit] - repo: https://github.com/kynan/nbstripout rev: 0.8.1 From bc29cd667445660a9cdf3bcc8abebdceb0606a6b Mon Sep 17 00:00:00 2001 From: Dominic Istha Date: Wed, 10 Sep 2025 14:09:05 +0200 Subject: [PATCH 046/199] updates setup.sh to accommodate for Linux and WSL in one script --- scripts/setup.sh | 271 +++++++++++++++++++++++------------------------ 1 file changed, 134 insertions(+), 137 deletions(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 9f170421..3f51ade0 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,207 +1,204 @@ #!/usr/bin/env bash set -euo pipefail -# Complete development environment setup script for Python Basic Template -# This script sets up everything a developer needs to start working on the project: -# - Virtual environment -# - Hybrid dependency management (production + development) -# - Pre-commit hooks -# - Secret management with 1Password -# - Development tools configuration - -# Colors +# configuration +export VENV_DIR="${VENV_DIR:-venv}" +export PYPROJECT="${PYPROJECT:-pyproject.toml}" +export PROD_LOCK="${PROD_LOCK:-requirements.txt}" +export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" + +# ui helpers GREEN='\033[0;32m' CYAN='\033[1;36m' YELLOW='\033[1;33m' RED='\033[0;31m' BLUE='\033[0;34m' RESET='\033[0m' - step() { echo -e "${CYAN}๐Ÿ‘‰ $1${RESET}"; } success(){ echo -e "${GREEN}โœ… $1${RESET}"; } -warn() { echo -e "${YELLOW}โš ๏ธ $1${RESET}"; } +warn() { echo -e "${YELLOW}โš  $1${RESET}"; } error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } -info() { echo -e "${BLUE}โ„น๏ธ $1${RESET}"; } +info() { echo -e "${BLUE}โ„น $1${RESET}"; } section(){ echo -e "\n${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" echo -e "${BLUE}๐Ÿ”ง $1${RESET}" echo -e "${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" } +is_wsl() { + # true if running inside WSL + grep -qi "microsoft" /proc/sys/kernel/osrelease 2>/dev/null || [[ -n "${WSL_DISTRO_NAME:-}" ]] +} + +has_cmd() { command -v "$1" >/dev/null 2>&1; } + setup_development_environment() { - section "SYSTEM REQUIREMENTS CHECK" - step "Verifying required system packages..." - info "Checking for Python 3..." - command -v python3 >/dev/null || error "Python 3 is not installed. Please install Python 3.12+." - PYTHON_VERSION=$(python3 --version | cut -d' ' -f2) - success "Python $PYTHON_VERSION found." - - info "Checking for pip..." - command -v pip >/dev/null || error "pip is not installed. Please install pip." - PIP_VERSION=$(pip --version | cut -d' ' -f2) - success "pip $PIP_VERSION found." - - info "Checking for curl..." - command -v curl >/dev/null || error "curl is not installed. Please install curl." - success "curl is available." - - info "Checking for git..." - command -v git >/dev/null || error "git is not installed. Please install git." - GIT_VERSION=$(git --version | cut -d' ' -f3) - success "git $GIT_VERSION found." - - section "VIRTUAL ENVIRONMENT SETUP" - step "Creating Python virtual environment..." + section "system requirements check" + step "verifying required system packages" + has_cmd python3 || error "Python 3 is not installed. Install Python 3.12+." + success "Python $(python3 --version | cut -d' ' -f2) found." + has_cmd pip || error "pip is not installed." + success "pip $(pip --version | cut -d' ' -f2) found." + has_cmd git || error "git is not installed." + success "git $(git --version | cut -d' ' -f3) found." + if has_cmd curl; then success "curl available."; else warn "curl not found. Some optional steps may be skipped."; fi + + section "virtual environment setup" + step "creating Python virtual environment" if [ -d "$VENV_DIR" ]; then - warn "Virtual environment already exists." - echo -e "${YELLOW}Choose an option:${RESET}" - echo " 1) Skip creating a new virtual environment (keep existing)" - echo " 2) Remove old one and create a new virtual environment" - read -p "Enter 1 or 2 [default: 2]: " VENV_CHOICE + warn "virtual environment already exists" + read -p "Enter 1 to keep, 2 to recreate [default: 2]: " VENV_CHOICE VENV_CHOICE=${VENV_CHOICE:-2} if [ "$VENV_CHOICE" = "1" ]; then - success "Keeping existing virtual environment." - source $VENV_DIR/bin/activate + success "keeping existing virtual environment" + # shellcheck disable=SC1090 + source "$VENV_DIR/bin/activate" elif [ "$VENV_CHOICE" = "2" ]; then - warn "Removing old virtual environment..." - rm -rf $VENV_DIR - python3 -m venv $VENV_DIR - source $VENV_DIR/bin/activate - success "Virtual environment created and activated." + warn "removing old virtual environment" + rm -rf "$VENV_DIR" + python3 -m venv "$VENV_DIR" + # shellcheck disable=SC1090 + source "$VENV_DIR/bin/activate" + success "virtual environment created and activated" else - error "Invalid choice. Exiting." + error "invalid choice" fi else - python3 -m venv $VENV_DIR - source $VENV_DIR/bin/activate - success "Virtual environment created and activated." + python3 -m venv "$VENV_DIR" + # shellcheck disable=SC1090 + source "$VENV_DIR/bin/activate" + success "virtual environment created and activated" fi - step "Upgrading pip in virtual environment..." - pip install --upgrade pip - PIP_NEW_VERSION=$(pip --version | cut -d' ' -f2) - success "pip upgraded to version $PIP_NEW_VERSION." - - section "HYBRID DEPENDENCY MANAGEMENT" - info "This project uses a hybrid dependency approach:" - info " โ€ข Production deps : declared in $PYPROJECT โ†’ locked to $PROD_LOCK" - info " โ€ข Development deps : declared in $PYPROJECT [project.optional-dependencies.dev] โ†’ locked to $DEV_LOCK" + step "upgrading pip in virtual environment" + python -m pip install --upgrade pip + success "pip upgraded to $(pip --version | cut -d' ' -f2)." - step "Installing dependency management tools (pip-tools, build, wheel)..." + section "hybrid dependency management" + info "production deps in $PYPROJECT to $PROD_LOCK" + info "development deps in $PYPROJECT extra [dev] to $DEV_LOCK" + step "installing pip tools" pip install pip-tools build wheel - success "pip-tools, build, and wheel installed." + success "pip tools installed" - step "Running dependency setup script (compiles & installs deps)..." + step "running dependency setup script" ./scripts/dependency.sh - success "Production and development dependencies installed." - - section "SYSTEM LOCALE CONFIGURATION" - step "Updating system locales..." - sudo apt update - sudo apt install -y locales - sudo locale-gen en_US.UTF-8 - sudo update-locale LANG=en_US.UTF-8 - success "System locales configured." - - section "DIRENV SETUP" - step "Installing direnv for environment variable management..." - sudo apt install -y direnv - success "direnv installed." - step "Configuring direnv shell integration..." - PROFILE="${HOME}/.bashrc" - if ! grep -q 'direnv hook bash' "$PROFILE"; then - echo 'eval "$(direnv hook bash)"' >> "$PROFILE" - success "Added direnv hook to $PROFILE." + success "dependencies installed" + + section "system locale configuration" + if has_cmd apt; then + step "ensuring locales" + sudo apt update + sudo apt install -y locales + sudo locale-gen en_US.UTF-8 + sudo update-locale LANG=en_US.UTF-8 + success "locales configured" else - warn "direnv hook already present in $PROFILE." + warn "apt not available. skipping locale setup" fi - success "Shell configuration reloaded." - step "Enabling direnv for this project..." - if [ -f ".envrc" ]; then - direnv allow - success "direnv enabled for this project." + + section "direnv setup" + if has_cmd apt; then + step "installing direnv" + sudo apt install -y direnv + success "direnv installed" + step "configuring direnv shell integration" + PROFILE="${HOME}/.bashrc" + if ! grep -q 'direnv hook bash' "$PROFILE"; then + echo 'eval "$(direnv hook bash)"' >> "$PROFILE" + success "added direnv hook to $PROFILE" + else + warn "direnv hook already present in $PROFILE" + fi + step "enabling direnv for this project" + if [ -f ".envrc" ]; then + direnv allow || true + success "direnv enabled for this project" + else + warn ".envrc not found. skipping allow" + fi else - warn ".envrc file not found. direnv configuration may be incomplete." + warn "apt not available. install direnv manually if desired" fi - section "PRE-COMMIT HOOKS SETUP" - step "Installing pre-commit hooks..." + section "pre commit hooks setup" + step "installing pre commit hooks" if [[ -f ".pre-commit-config.yaml" ]]; then pre-commit install - success "Pre-commit hooks installed." - step "Updating pre-commit hook repositories..." + success "pre commit hooks installed" + step "updating pre commit repos" pre-commit autoupdate - success "Pre-commit hooks updated." - step "Running pre-commit on all files (initial check)..." - pre-commit run --all-files || warn "Some pre-commit checks failed (initial run)." - info "Pre-commit will now run automatically on commits." + success "pre commit hooks updated" + step "running pre commit on all files" + pre-commit run --all-files || warn "some pre commit checks failed" else - warn ".pre-commit-config.yaml not found. Skipping hooks install." + warn ".pre-commit-config.yaml not found. skipping" fi - section "SECRET MANAGEMENT SETUP" - step "Checking 1Password CLI integration..." - if op --version &>/dev/null; then - success "1Password CLI is installed." - step "Testing 1Password connection..." - if op account list &>/dev/null; then - success "1Password CLI is connected." + section "secret management setup" + step "checking 1password cli" + if has_cmd op; then + success "1password cli is installed" + if op account list >/dev/null 2>&1; then + success "1password cli is connected" else - warn "1Password CLI not signed in." + warn "1password cli not signed in" fi else - error "1Password CLI not found. Install & configure before secrets workflow." - exit 1 + # In WSL users sometimes rely on Windows 1Password + if is_wsl; then + warn "op not found in WSL. if you use Windows 1Password, install op in WSL or skip this" + else + error "1Password CLI not found. install and configure before secrets workflow" + fi fi - step "Initializing secrets baseline with detect-secrets..." + step "initializing secrets baseline" if [ -f ".secrets.baseline" ]; then - warn "Existing .secrets.baseline found; backing up." + warn "existing .secrets.baseline found. backing up" cp .secrets.baseline .secrets.baseline.bak fi detect-secrets scan > .secrets.baseline - success "Secrets baseline created." - detect-secrets audit .secrets.baseline - success "Secrets configuration verified." + success "secrets baseline created" + detect-secrets audit .secrets.baseline || warn "manual review of baseline recommended" + success "secrets configuration verified" - section "DEVELOPMENT TOOLS VERIFICATION" - step "Verifying development tools..." + section "development tools verification" + step "verifying development tools" for tool in black ruff mypy pytest beartype; do - info "Checking $tool..." - "pip show $tool" &>/dev/null && success "$tool is installed." || warn "$tool missing." + if pip show "$tool" >/dev/null 2>&1; then success "$tool installed"; else warn "$tool missing"; fi done - section "PROJECT VALIDATION" - step "Running final project validation..." - info "Checking imports..." + section "project validation" + step "running final project validation" + info "checking imports" python - <<'PYCODE' import utils.secrets, utils.utils print("โœ… All project modules import successfully") PYCODE - info "Syntax-checking all Python files..." + info "syntax checking" find . -name "*.py" -not -path "./$VENV_DIR/*" -exec python -m py_compile {} \; - success "Python syntax valid." + success "python syntax valid" if [ -s "$PROD_LOCK" ]; then - info "Verifying production dependencies..." + info "verifying dependency compatibility" pip check - success "Dependencies compatible." + success "dependencies compatible" else - info "No prod dependencies to verify." + info "no production dependencies to verify" fi - section "SETUP COMPLETE" - echo -e "\n${GREEN}๐ŸŽ‰ DEVELOPMENT ENVIRONMENT SETUP COMPLETE!${RESET}\n" - echo -e "${BLUE}Installed:${RESET}" + section "setup complete" + echo -e "\n${GREEN}๐ŸŽ‰ development environment setup complete${RESET}\n" + echo -e "${BLUE}installed:${RESET}" echo " โ€ข $VENV_DIR/" - echo " โ€ข Production deps ($PROD_LOCK)" - echo " โ€ข Dev tools ($DEV_LOCK)" - echo " โ€ข Pre-commit hooks" - echo " โ€ข Secret baseline (.secrets.baseline)" + echo " โ€ข production deps ($PROD_LOCK)" + echo " โ€ข dev tools ($DEV_LOCK)" + echo " โ€ข pre commit hooks" + echo " โ€ข secret baseline (.secrets.baseline)" echo " โ€ข direnv" - echo -e "\n${BLUE}Next steps:${RESET}" - echo " 1. Restart terminal or run: source ~/.bashrc" - echo " 2. Test with: python example/using_secrets.py" - echo " 3. Start coding!" + echo -e "\n${BLUE}next steps:${RESET}" + echo " 1. restart terminal or run: source ~/.bashrc" + echo " 2. test with: python example/using_secrets.py" + echo " 3. start coding" } -# Main setup_development_environment From 183292f4dcae218fe4ea65276473ea749fcb0b98 Mon Sep 17 00:00:00 2001 From: Dominic Istha Date: Wed, 10 Sep 2025 14:12:59 +0200 Subject: [PATCH 047/199] minor changes to setup.ps1 --- scripts/setup.ps1 | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index dddf5bb4..9311ed3a 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -26,7 +26,7 @@ Ok "git: $(git --version)" # 2. venv Step "Creating and activating virtual environment" -$venv = $env:VENV_DIR +$venv = if ($env:VENV_DIR) { $env:VENV_DIR } else { "venv" } if(Test-Path $venv){ Warn "Existing venv found, reusing" } else { python -m venv $venv } $activate = Join-Path $venv "Scripts\Activate.ps1" . $activate @@ -34,7 +34,7 @@ Ok "Virtual environment activated" # 3. pip tooling Step "Upgrading pip and wheel" -python -m pip install --upgrade pip wheel build +python -m pip install --upgrade pip wheel build pip-tools # 4. dependencies (calls dependency.ps1) if($DevOnly){ .\scripts\dependency.ps1 --dev } @@ -44,25 +44,30 @@ else { .\scripts\dependency.ps1 } # 5. developer tools Step "Installing developer tools" pip install pre-commit detect-secrets nbstripout pip-audit jupytext pytest pytest-cov pytest-beartype beartype +try { python -m piptools --version | Out-Null } catch { Warn "pip-tools not on PATH yet; pre-commit hooks may fail until a new shell is opened" } # 6. Installing project Step "Installing project in editable mode" pip install -e . -# 6. pre-commit hooks +# 7. pre-commit hooks Step "Installing pre-commit hooks" -pre-commit install -c .pre-commit-windows.yaml -pre-commit autoupdate -c .pre-commit-windows.yaml -try { pre-commit run --all-files -c .pre-commit-windows.yaml } catch { Warn "Pre-commit found issues, fix then re-run" } +if (Test-Path ".pre-commit-windows.yaml") { + pre-commit install -c .pre-commit-windows.yaml + pre-commit autoupdate -c .pre-commit-windows.yaml + try { pre-commit run --all-files -c .pre-commit-windows.yaml } catch { Warn "Pre-commit found issues, fix then re-run" } +} else { + Warn ".pre-commit-windows.yaml not found, skipping pre-commit setup" +} -# 7. Install nbstripout +# 8. Install nbstripout try { .\scripts\hooks\nbstripout-install.ps1 } catch { Warn "nbstripout install skipped: $($_.Exception.Message)" } -# 8. validation +# 9. validation Step "Running import smoke test" $env:PYTHONPATH = (Get-Location).Path python -c "import utils.secrets; print('โœ… All project modules import successfully')" From a096784cccd00666bcf26e28d47df35d02f0c94a Mon Sep 17 00:00:00 2001 From: Dominic Istha Date: Wed, 10 Sep 2025 14:23:01 +0200 Subject: [PATCH 048/199] update readme with instructions for windows powershell execution policies --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index e468f1d2..34b35fa2 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,30 @@ cd python_basic_template This will set up everything you need for development! +### Common setup issues + +#### Issue: Windows setup Powershell Execution Policy + +If you see an error like: + +``` +File .\setup.ps1 cannot be loaded. The file .\setup.ps1 is not digitally signed. You cannot run this script on the current system. For more information about running scripts and setting execution policy, see about_Execution_Policies at https:/go.microsoft.com/fwlink/?LinkID=135170. +``` + +Windows is blocking unsigned scripts by default. To fix this: + +1. Open PowerShell **as your normal user** (no need for admin). +2. Run: + ```powershell + Set-ExecutionPolicy -Scope CurrentUser RemoteSigned +Restart PowerShell and try again: + +```powershell +.\scripts\setup.ps1 +``` + +This only changes the policy for your user account. It allows you to run local scripts while still requiring downloaded scripts to be unblocked (Unblock-File) before execution. + ## ๐Ÿ“‹ Features ### ๐Ÿ”ง **Modern Dependency Management** From 8ab6d537344808718572c62f90c271da5e6ce539 Mon Sep 17 00:00:00 2001 From: Dominic Istha Date: Wed, 10 Sep 2025 14:28:53 +0200 Subject: [PATCH 049/199] check --- .pre-commit-windows.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-windows.yaml b/.pre-commit-windows.yaml index 65d94888..4e324437 100644 --- a/.pre-commit-windows.yaml +++ b/.pre-commit-windows.yaml @@ -40,7 +40,7 @@ repos: exclude: ^tests/ - repo: https://github.com/mwouts/jupytext - rev: v1.17.2 + rev: v1.17.3 hooks: - id: jupytext args: [--sync] @@ -53,7 +53,7 @@ repos: args: ["--config-file=pyproject.toml"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.8 + rev: v0.12.12 hooks: - id: ruff args: [--fix] From 6ee936105627a9ee63d95933db8fafccc1da2d08 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 10 Sep 2025 15:36:35 +0200 Subject: [PATCH 050/199] Remove redundant hooks --- scripts/hooks/verify-dev-lockfile.sh | 3 --- scripts/hooks/verify-prod-lockfile.sh | 3 --- 2 files changed, 6 deletions(-) delete mode 100755 scripts/hooks/verify-dev-lockfile.sh delete mode 100755 scripts/hooks/verify-prod-lockfile.sh diff --git a/scripts/hooks/verify-dev-lockfile.sh b/scripts/hooks/verify-dev-lockfile.sh deleted file mode 100755 index c6ce08e1..00000000 --- a/scripts/hooks/verify-dev-lockfile.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -"$(dirname "$0")/verify-lockfile.sh" requirements-dev.txt diff --git a/scripts/hooks/verify-prod-lockfile.sh b/scripts/hooks/verify-prod-lockfile.sh deleted file mode 100755 index 2107b477..00000000 --- a/scripts/hooks/verify-prod-lockfile.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -"$(dirname "$0")/verify-lockfile.sh" requirements.txt From 53e312c42cb8e9f25effb4d79351dba03b8d6ae7 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 12 Sep 2025 09:07:47 +0200 Subject: [PATCH 051/199] Update setup-linux.sh --- scripts/setup-linux.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/setup-linux.sh b/scripts/setup-linux.sh index 52ae0e0c..893a992b 100755 --- a/scripts/setup-linux.sh +++ b/scripts/setup-linux.sh @@ -2,7 +2,7 @@ set -euo pipefail # configuration -export VENV_DIR="venv" +export VENV_DIR=".venv" export PYPROJECT="pyproject.toml" export PROD_LOCK="requirements.txt" export DEV_LOCK="requirements-dev.txt" From 50ef9aac378e0652f9a260991779a8e6c6867693 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 12 Sep 2025 09:08:40 +0200 Subject: [PATCH 052/199] Update .envrc --- .envrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.envrc b/.envrc index 959bf71f..1b795365 100644 --- a/.envrc +++ b/.envrc @@ -6,7 +6,7 @@ if [ -f .env ]; then fi export PYTHONPATH=. -export VENV_DIR="venv" +export VENV_DIR=".venv" export PROD_LOCK="requirements.txt" export DEV_LOCK="requirements-dev.txt" export PYPROJECT="pyproject.toml" From 9040c7c3864bd701d9041eafa86764ccdf6f9592 Mon Sep 17 00:00:00 2001 From: MLingerTechletes Date: Fri, 12 Sep 2025 14:04:21 +0200 Subject: [PATCH 053/199] Restore /scripts folder from commit 8ab6d53 --- scripts/dependency.sh | 112 +++----------- scripts/hooks/verify-dev-lockfile.sh | 3 + scripts/hooks/verify-lockfile.sh | 0 scripts/hooks/verify-prod-lockfile.sh | 3 + scripts/setup-linux.sh | 213 -------------------------- scripts/{setup-wsl.sh => setup.sh} | 0 scripts/verify-lockfile.sh | 38 ----- 7 files changed, 30 insertions(+), 339 deletions(-) create mode 100644 scripts/hooks/verify-dev-lockfile.sh mode change 100755 => 100644 scripts/hooks/verify-lockfile.sh create mode 100644 scripts/hooks/verify-prod-lockfile.sh delete mode 100755 scripts/setup-linux.sh rename scripts/{setup-wsl.sh => setup.sh} (100%) mode change 100644 => 100755 delete mode 100755 scripts/verify-lockfile.sh diff --git a/scripts/dependency.sh b/scripts/dependency.sh index ac1a1492..91e9b47a 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -1,49 +1,21 @@ #!/usr/bin/env bash set -euo pipefail -# ----------------------------------------------------------------------------- -# Configuration -# ----------------------------------------------------------------------------- -# Use environment variables if they exist, otherwise set sensible defaults. -export VENV_DIR="${VENV_DIR:-venv}" -export PYPROJECT="${PYPROJECT:-pyproject.toml}" -export PROD_LOCK="${PROD_LOCK:-requirements.txt}" -export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" - -# ----------------------------------------------------------------------------- -# Script Info -# ----------------------------------------------------------------------------- # Hybrid dependency management script: -# Compiles lockfiles first, then installs, to ensure a robust workflow. -# # Usage: -# ./scripts/dependency.sh # Compile and install both prod & dev -# ./scripts/dependency.sh --prod # Compile and install only prod -# ./scripts/dependency.sh --dev # Compile and install only dev +# ./dependency.sh # install both prod & dev +# ./dependency.sh --prod # install only production deps +# ./dependency.sh --dev # install only development deps -# ----------------------------------------------------------------------------- -# UI Functions -# ----------------------------------------------------------------------------- # Colors YELLOW='\033[1;33m' GREEN='\033[1;32m' -BLUE='\033[0;34m' -RED='\033[0;31m' RESET='\033[0m' -# Helper functions for printing formatted output -info() { echo -e "${BLUE}โ„น๏ธ $1${RESET}"; } -step() { echo -e "\n${YELLOW}$1${RESET}"; } -success() { echo -e "${GREEN}โœ… $1${RESET}"; } -error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } - -# ----------------------------------------------------------------------------- -# Argument Parsing -# ----------------------------------------------------------------------------- +# Parse args INSTALL_PROD=false INSTALL_DEV=false -# Default to both if no arguments are provided if [ $# -eq 0 ]; then INSTALL_PROD=true INSTALL_DEV=true @@ -57,92 +29,56 @@ else INSTALL_DEV=true ;; *) - error "Unknown option: $arg. Usage: $0 [--prod] [--dev]" + echo "โŒ Unknown option: $arg" + echo "Usage: $0 [--prod] [--dev]" + exit 1 ;; esac done fi -# ----------------------------------------------------------------------------- -# Environment Setup -# ----------------------------------------------------------------------------- -step "Activating virtualenv at '${VENV_DIR}'..." +echo -e "${YELLOW}โณ Activating virtualenv at '${VENV_DIR}'...${RESET}" if [ -f "${VENV_DIR}/bin/activate" ]; then # shellcheck disable=SC1090 source "${VENV_DIR}/bin/activate" - success "Virtual environment activated." else - error "Virtualenv not found at '${VENV_DIR}'. Please run setup script first." + echo "โŒ Virtualenv not found at '${VENV_DIR}'." + echo " Create one with: python -m venv ${VENV_DIR}" + exit 1 fi -step "Ensuring pip-tools is installed..." +echo -e "${YELLOW}๐Ÿ” Ensuring pip-tools is installed...${RESET}" if ! pip show pip-tools &>/dev/null; then - info "Installing pip-tools..." + echo -e "${YELLOW}โžก๏ธ Installing pip-tools...${RESET}" pip install pip-tools - success "pip-tools installed." else - success "pip-tools is already installed." + echo -e "${GREEN}โœ… pip-tools is already installed.${RESET}" fi -# ============================================================================= -# MAJOR CHANGE: The script is now split into a COMPILE phase and an INSTALL phase. -# ============================================================================= - -# ----------------------------------------------------------------------------- -# PHASE 1: COMPILE ALL REQUIRED LOCKFILES -# ----------------------------------------------------------------------------- -# This block runs first to ensure our lockfiles are valid before we try to -# install from them. This prevents errors if a file was manually edited. - -step "PHASE 1: Compiling lockfiles from '${PYPROJECT}'..." - if [ "$INSTALL_PROD" = true ]; then - info "Compiling production lockfile: '${PROD_LOCK}'" + echo -e "${YELLOW}๐Ÿงฎ Compiling production lockfile from '${PYPROJECT}'...${RESET}" pip-compile "${PYPROJECT}" \ --output-file="${PROD_LOCK}" \ --generate-hashes + + echo -e "${YELLOW}๐Ÿ“ฅ Installing production dependencies...${RESET}" + pip install --require-hashes -r "${PROD_LOCK}" fi if [ "$INSTALL_DEV" = true ]; then - info "Compiling development lockfile: '${DEV_LOCK}'" + echo -e "${YELLOW}๐Ÿ›  Compiling development lockfile from '${PYPROJECT}' extras...${RESET}" pip-compile "${PYPROJECT}" \ --extra=dev \ --output-file="${DEV_LOCK}" \ --generate-hashes -fi - -success "All required lockfiles compiled." -# ----------------------------------------------------------------------------- -# PHASE 2: INSTALL DEPENDENCIES FROM THE (NOW VERIFIED) LOCKFILES -# ----------------------------------------------------------------------------- -# Now that we know the lockfiles are correct, we can safely install from them. - -step "PHASE 2: Installing dependencies..." - -if [ "$INSTALL_PROD" = true ]; then - info "Installing production dependencies from '${PROD_LOCK}'..." - # We use --require-hashes for a secure install. - pip install --require-hashes -r "${PROD_LOCK}" -fi - -if [ "$INSTALL_DEV" = true ]; then - info "Installing development dependencies from '${DEV_LOCK}'..." - # The dev requirements should include the prod ones, but `pip install` handles duplicates gracefully. + echo -e "${YELLOW}๐Ÿ“ฅ Installing development dependencies...${RESET}" pip install --require-hashes -r "${DEV_LOCK}" fi -success "All required dependencies installed." - -# ----------------------------------------------------------------------------- -# Final Verification -# ----------------------------------------------------------------------------- -step "Verifying environment consistency..." +echo -e "${YELLOW}๐Ÿ”Ž Verifying installed packages...${RESET}" pip check -success "Environment check passed." -# --- Final Summary --- -echo -success "Dependency setup complete!" -[ "$INSTALL_PROD" = true ] && echo " โ€ข Production dependencies updated from '${PROD_LOCK}'" -[ "$INSTALL_DEV" = true ] && echo " โ€ข Development dependencies updated from '${DEV_LOCK}'" +echo -e "${GREEN}๐ŸŽ‰ Setup complete!${RESET}" +[ "$INSTALL_PROD" = true ] && echo -e "${GREEN} โ€ข Production deps from ${PROD_LOCK}${RESET}" +[ "$INSTALL_DEV" = true ] && echo -e "${GREEN} โ€ข Dev deps from ${DEV_LOCK}${RESET}" \ No newline at end of file diff --git a/scripts/hooks/verify-dev-lockfile.sh b/scripts/hooks/verify-dev-lockfile.sh new file mode 100644 index 00000000..c6ce08e1 --- /dev/null +++ b/scripts/hooks/verify-dev-lockfile.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +"$(dirname "$0")/verify-lockfile.sh" requirements-dev.txt diff --git a/scripts/hooks/verify-lockfile.sh b/scripts/hooks/verify-lockfile.sh old mode 100755 new mode 100644 diff --git a/scripts/hooks/verify-prod-lockfile.sh b/scripts/hooks/verify-prod-lockfile.sh new file mode 100644 index 00000000..2107b477 --- /dev/null +++ b/scripts/hooks/verify-prod-lockfile.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +"$(dirname "$0")/verify-lockfile.sh" requirements.txt diff --git a/scripts/setup-linux.sh b/scripts/setup-linux.sh deleted file mode 100755 index 893a992b..00000000 --- a/scripts/setup-linux.sh +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# configuration -export VENV_DIR=".venv" -export PYPROJECT="pyproject.toml" -export PROD_LOCK="requirements.txt" -export DEV_LOCK="requirements-dev.txt" - -# Complete development environment setup script for Python Basic Template -# This script sets up everything a developer needs to start working on the project: -# - Virtual environment -# - Hybrid dependency management (production + development) -# - Pre-commit hooks -# - Secret management with 1Password -# - Development tools configuration - -# Colors -GREEN='\033[0;32m' -CYAN='\033[1;36m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -BLUE='\033[0;34m' -RESET='\033[0m' - -step() { echo -e "${CYAN}๐Ÿ‘‰ $1${RESET}"; } -success(){ echo -e "${GREEN}โœ… $1${RESET}"; } -warn() { echo -e "${YELLOW}โš ๏ธ $1${RESET}"; } -error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } -info() { echo -e "${BLUE}โ„น๏ธ $1${RESET}"; } -section(){ - echo -e "\n${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" - echo -e "${BLUE}๐Ÿ”ง $1${RESET}" - echo -e "${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" -} - -setup_development_environment() { - section "SYSTEM REQUIREMENTS CHECK" - step "Verifying required system packages..." - info "Checking for Python 3..." - command -v python3 >/dev/null || error "Python 3 is not installed. Please install Python 3.12+." - PYTHON_VERSION=$(python3 --version | cut -d' ' -f2) - success "Python $PYTHON_VERSION found." - - info "Checking for pip..." - command -v pip >/dev/null || error "pip is not installed. Please install pip." - PIP_VERSION=$(pip --version | cut -d' ' -f2) - success "pip $PIP_VERSION found." - - info "Checking for curl..." - command -v curl >/dev/null || error "curl is not installed. Please install curl." - success "curl is available." - - info "Checking for git..." - command -v git >/dev/null || error "git is not installed. Please install git." - GIT_VERSION=$(git --version | cut -d' ' -f3) - success "git $GIT_VERSION found." - - section "VIRTUAL ENVIRONMENT SETUP" - step "Creating Python virtual environment..." - if [ -d "$VENV_DIR" ]; then - warn "Virtual environment already exists." - echo -e "${YELLOW}Choose an option:${RESET}" - echo " 1) Skip creating a new virtual environment (keep existing)" - echo " 2) Remove old one and create a new virtual environment" - read -p "Enter 1 or 2 [default: 2]: " VENV_CHOICE - VENV_CHOICE=${VENV_CHOICE:-2} - if [ "$VENV_CHOICE" = "1" ]; then - success "Keeping existing virtual environment." - source $VENV_DIR/bin/activate - elif [ "$VENV_CHOICE" = "2" ]; then - warn "Removing old virtual environment..." - rm -rf $VENV_DIR - python3 -m venv $VENV_DIR - source $VENV_DIR/bin/activate - success "Virtual environment created and activated." - else - error "Invalid choice. Exiting." - fi - else - python3 -m venv $VENV_DIR - source $VENV_DIR/bin/activate - success "Virtual environment created and activated." - fi - - step "Upgrading pip in virtual environment..." - pip install --upgrade pip - PIP_NEW_VERSION=$(pip --version | cut -d' ' -f2) - success "pip upgraded to version $PIP_NEW_VERSION." - - section "HYBRID DEPENDENCY MANAGEMENT" - info "This project uses a hybrid dependency approach:" - info " โ€ข Production deps : declared in $PYPROJECT โ†’ locked to $PROD_LOCK" - info " โ€ข Development deps : declared in $PYPROJECT [project.optional-dependencies.dev] โ†’ locked to $DEV_LOCK" - - step "Installing dependency management tools (pip-tools, build, wheel)..." - pip install pip-tools build wheel - success "pip-tools, build, and wheel installed." - - step "Running dependency setup script (compiles & installs deps)..." - ./scripts/dependency.sh - success "Production and development dependencies installed." - - section "SYSTEM LOCALE CONFIGURATION" - step "Updating system locales..." - sudo apt update - sudo apt install -y locales - sudo locale-gen en_US.UTF-8 - sudo update-locale LANG=en_US.UTF-8 - success "System locales configured." - - section "DIRENV SETUP" - step "Installing direnv for environment variable management..." - sudo apt install -y direnv - success "direnv installed." - step "Configuring direnv shell integration..." - PROFILE="${HOME}/.bashrc" - if ! grep -q 'direnv hook bash' "$PROFILE"; then - echo 'eval "$(direnv hook bash)"' >> "$PROFILE" - success "Added direnv hook to $PROFILE." - else - warn "direnv hook already present in $PROFILE." - fi - success "Shell configuration reloaded." - step "Enabling direnv for this project..." - if [ -f ".envrc" ]; then - direnv allow - success "direnv enabled for this project." - else - warn ".envrc file not found. direnv configuration may be incomplete." - fi - - section "PRE-COMMIT HOOKS SETUP" - step "Installing pre-commit hooks..." - if [[ -f ".pre-commit-config.yaml" ]]; then - pre-commit install - success "Pre-commit hooks installed." - step "Updating pre-commit hook repositories..." - pre-commit autoupdate - success "Pre-commit hooks updated." - step "Running pre-commit on all files (initial check)..." - pre-commit run --all-files || warn "Some pre-commit checks failed (initial run)." - info "Pre-commit will now run automatically on commits." - else - warn ".pre-commit-config.yaml not found. Skipping hooks install." - fi - - section "SECRET MANAGEMENT SETUP" - step "Checking 1Password CLI integration..." - if op --version &>/dev/null; then - success "1Password CLI is installed." - step "Testing 1Password connection..." - if op account list &>/dev/null; then - success "1Password CLI is connected." - else - warn "1Password CLI not signed in." - fi - else - error "1Password CLI not found. Install & configure before secrets workflow." - exit 1 - fi - step "Initializing secrets baseline with detect-secrets..." - if [ -f ".secrets.baseline" ]; then - warn "Existing .secrets.baseline found; backing up." - cp .secrets.baseline .secrets.baseline.bak - fi - detect-secrets scan > .secrets.baseline - success "Secrets baseline created." - detect-secrets audit .secrets.baseline - success "Secrets configuration verified." - - section "DEVELOPMENT TOOLS VERIFICATION" - step "Verifying development tools..." - for tool in black ruff mypy pytest beartype; do - info "Checking $tool..." - "pip show $tool" &>/dev/null && success "$tool is installed." || warn "$tool missing." - done - - section "PROJECT VALIDATION" - step "Running final project validation..." - info "Checking imports..." - python - <<'PYCODE' -import utils.secrets, utils.utils -print("โœ… All project modules import successfully") -PYCODE - info "Syntax-checking all Python files..." - find . -name "*.py" -not -path "./$VENV_DIR/*" -exec python -m py_compile {} \; - success "Python syntax valid." - if [ -s "$PROD_LOCK" ]; then - info "Verifying production dependencies..." - pip check - success "Dependencies compatible." - else - info "No prod dependencies to verify." - fi - - section "SETUP COMPLETE" - echo -e "\n${GREEN}๐ŸŽ‰ DEVELOPMENT ENVIRONMENT SETUP COMPLETE!${RESET}\n" - echo -e "${BLUE}Installed:${RESET}" - echo " โ€ข $VENV_DIR/" - echo " โ€ข Production deps ($PROD_LOCK)" - echo " โ€ข Dev tools ($DEV_LOCK)" - echo " โ€ข Pre-commit hooks" - echo " โ€ข Secret baseline (.secrets.baseline)" - echo " โ€ข direnv" - echo -e "\n${BLUE}Next steps:${RESET}" - echo " 1. Restart terminal or run: source ~/.bashrc" - echo " 2. Test with: python example/using_secrets.py" - echo " 3. Start coding!" -} - -# Main -setup_development_environment diff --git a/scripts/setup-wsl.sh b/scripts/setup.sh old mode 100644 new mode 100755 similarity index 100% rename from scripts/setup-wsl.sh rename to scripts/setup.sh diff --git a/scripts/verify-lockfile.sh b/scripts/verify-lockfile.sh deleted file mode 100755 index 38616028..00000000 --- a/scripts/verify-lockfile.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# This script verifies that a lockfile is up-to-date with pyproject.toml. -# It's designed to be called by a pre-commit hook. -# It accepts one argument: the path to the lockfile to check (e.g., requirements.txt) - -# --- Configuration --- -LOCKFILE_TO_CHECK="$1" -PYPROJECT_FILE="pyproject.toml" -TEMP_LOCKFILE=$(mktemp) # Creates a secure temporary file - -# --- Logic --- -echo "Verifying integrity of '$LOCKFILE_TO_CHECK'..." - -# Determine which compile command to run based on the filename -if [[ "$LOCKFILE_TO_CHECK" == "requirements-dev.txt" ]]; then - # Compile development dependencies - pip-compile "$PYPROJECT_FILE" --extra=dev --output-file="$TEMP_LOCKFILE" > /dev/null -else - # Compile production dependencies by default - pip-compile "$PYPROJECT_FILE" --output-file="$TEMP_LOCKFILE" > /dev/null -fi - -# Compare the officially generated temp file with the one being committed. -# The `diff -q` command is quiet and exits with a non-zero status if files differ. -if ! diff -q "$LOCKFILE_TO_CHECK" "$TEMP_LOCKFILE"; then - echo "โŒ Error: '$LOCKFILE_TO_CHECK' has been manually edited or is out of date." >&2 - echo " It does not match the expected output based on '$PYPROJECT_FILE'." >&2 - echo " To fix this, run 'rm requirements.txt' and then './scripts/dependency.sh' and commit the resulting changes." >&2 - rm "$TEMP_LOCKFILE" # Always clean up - exit 1 -fi - -# If diff exits with 0, the files are identical. -echo "โœ… '$LOCKFILE_TO_CHECK' is valid and up-to-date." -rm "$TEMP_LOCKFILE" # Always clean up -exit 0 From c7a7aa576ed741228d67e0f22120c6300121f344 Mon Sep 17 00:00:00 2001 From: MLingerTechletes Date: Fri, 12 Sep 2025 14:35:38 +0200 Subject: [PATCH 054/199] keep main version of scripts/dependency.sh --- scripts/dependency.sh | 112 +++++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/scripts/dependency.sh b/scripts/dependency.sh index 91e9b47a..ac1a1492 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -1,21 +1,49 @@ #!/usr/bin/env bash set -euo pipefail +# ----------------------------------------------------------------------------- +# Configuration +# ----------------------------------------------------------------------------- +# Use environment variables if they exist, otherwise set sensible defaults. +export VENV_DIR="${VENV_DIR:-venv}" +export PYPROJECT="${PYPROJECT:-pyproject.toml}" +export PROD_LOCK="${PROD_LOCK:-requirements.txt}" +export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" + +# ----------------------------------------------------------------------------- +# Script Info +# ----------------------------------------------------------------------------- # Hybrid dependency management script: +# Compiles lockfiles first, then installs, to ensure a robust workflow. +# # Usage: -# ./dependency.sh # install both prod & dev -# ./dependency.sh --prod # install only production deps -# ./dependency.sh --dev # install only development deps +# ./scripts/dependency.sh # Compile and install both prod & dev +# ./scripts/dependency.sh --prod # Compile and install only prod +# ./scripts/dependency.sh --dev # Compile and install only dev +# ----------------------------------------------------------------------------- +# UI Functions +# ----------------------------------------------------------------------------- # Colors YELLOW='\033[1;33m' GREEN='\033[1;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' RESET='\033[0m' -# Parse args +# Helper functions for printing formatted output +info() { echo -e "${BLUE}โ„น๏ธ $1${RESET}"; } +step() { echo -e "\n${YELLOW}$1${RESET}"; } +success() { echo -e "${GREEN}โœ… $1${RESET}"; } +error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } + +# ----------------------------------------------------------------------------- +# Argument Parsing +# ----------------------------------------------------------------------------- INSTALL_PROD=false INSTALL_DEV=false +# Default to both if no arguments are provided if [ $# -eq 0 ]; then INSTALL_PROD=true INSTALL_DEV=true @@ -29,56 +57,92 @@ else INSTALL_DEV=true ;; *) - echo "โŒ Unknown option: $arg" - echo "Usage: $0 [--prod] [--dev]" - exit 1 + error "Unknown option: $arg. Usage: $0 [--prod] [--dev]" ;; esac done fi -echo -e "${YELLOW}โณ Activating virtualenv at '${VENV_DIR}'...${RESET}" +# ----------------------------------------------------------------------------- +# Environment Setup +# ----------------------------------------------------------------------------- +step "Activating virtualenv at '${VENV_DIR}'..." if [ -f "${VENV_DIR}/bin/activate" ]; then # shellcheck disable=SC1090 source "${VENV_DIR}/bin/activate" + success "Virtual environment activated." else - echo "โŒ Virtualenv not found at '${VENV_DIR}'." - echo " Create one with: python -m venv ${VENV_DIR}" - exit 1 + error "Virtualenv not found at '${VENV_DIR}'. Please run setup script first." fi -echo -e "${YELLOW}๐Ÿ” Ensuring pip-tools is installed...${RESET}" +step "Ensuring pip-tools is installed..." if ! pip show pip-tools &>/dev/null; then - echo -e "${YELLOW}โžก๏ธ Installing pip-tools...${RESET}" + info "Installing pip-tools..." pip install pip-tools + success "pip-tools installed." else - echo -e "${GREEN}โœ… pip-tools is already installed.${RESET}" + success "pip-tools is already installed." fi +# ============================================================================= +# MAJOR CHANGE: The script is now split into a COMPILE phase and an INSTALL phase. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# PHASE 1: COMPILE ALL REQUIRED LOCKFILES +# ----------------------------------------------------------------------------- +# This block runs first to ensure our lockfiles are valid before we try to +# install from them. This prevents errors if a file was manually edited. + +step "PHASE 1: Compiling lockfiles from '${PYPROJECT}'..." + if [ "$INSTALL_PROD" = true ]; then - echo -e "${YELLOW}๐Ÿงฎ Compiling production lockfile from '${PYPROJECT}'...${RESET}" + info "Compiling production lockfile: '${PROD_LOCK}'" pip-compile "${PYPROJECT}" \ --output-file="${PROD_LOCK}" \ --generate-hashes - - echo -e "${YELLOW}๐Ÿ“ฅ Installing production dependencies...${RESET}" - pip install --require-hashes -r "${PROD_LOCK}" fi if [ "$INSTALL_DEV" = true ]; then - echo -e "${YELLOW}๐Ÿ›  Compiling development lockfile from '${PYPROJECT}' extras...${RESET}" + info "Compiling development lockfile: '${DEV_LOCK}'" pip-compile "${PYPROJECT}" \ --extra=dev \ --output-file="${DEV_LOCK}" \ --generate-hashes +fi + +success "All required lockfiles compiled." - echo -e "${YELLOW}๐Ÿ“ฅ Installing development dependencies...${RESET}" +# ----------------------------------------------------------------------------- +# PHASE 2: INSTALL DEPENDENCIES FROM THE (NOW VERIFIED) LOCKFILES +# ----------------------------------------------------------------------------- +# Now that we know the lockfiles are correct, we can safely install from them. + +step "PHASE 2: Installing dependencies..." + +if [ "$INSTALL_PROD" = true ]; then + info "Installing production dependencies from '${PROD_LOCK}'..." + # We use --require-hashes for a secure install. + pip install --require-hashes -r "${PROD_LOCK}" +fi + +if [ "$INSTALL_DEV" = true ]; then + info "Installing development dependencies from '${DEV_LOCK}'..." + # The dev requirements should include the prod ones, but `pip install` handles duplicates gracefully. pip install --require-hashes -r "${DEV_LOCK}" fi -echo -e "${YELLOW}๐Ÿ”Ž Verifying installed packages...${RESET}" +success "All required dependencies installed." + +# ----------------------------------------------------------------------------- +# Final Verification +# ----------------------------------------------------------------------------- +step "Verifying environment consistency..." pip check +success "Environment check passed." -echo -e "${GREEN}๐ŸŽ‰ Setup complete!${RESET}" -[ "$INSTALL_PROD" = true ] && echo -e "${GREEN} โ€ข Production deps from ${PROD_LOCK}${RESET}" -[ "$INSTALL_DEV" = true ] && echo -e "${GREEN} โ€ข Dev deps from ${DEV_LOCK}${RESET}" \ No newline at end of file +# --- Final Summary --- +echo +success "Dependency setup complete!" +[ "$INSTALL_PROD" = true ] && echo " โ€ข Production dependencies updated from '${PROD_LOCK}'" +[ "$INSTALL_DEV" = true ] && echo " โ€ข Development dependencies updated from '${DEV_LOCK}'" From 4a628cd7dbab0a30422073d40a957580db1b39ac Mon Sep 17 00:00:00 2001 From: romantechletes Date: Fri, 12 Sep 2025 15:34:40 +0200 Subject: [PATCH 055/199] add steps to setup.sh to check for direnv/pip/python3.12-venv and install --- scripts/setup.sh | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 3f51ade0..42f799ab 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -26,23 +26,64 @@ section(){ } is_wsl() { - # true if running inside WSL grep -qi "microsoft" /proc/sys/kernel/osrelease 2>/dev/null || [[ -n "${WSL_DISTRO_NAME:-}" ]] } has_cmd() { command -v "$1" >/dev/null 2>&1; } +# Universal package installation function +install_package() { + local package_name=$1 + step "Attempting to install $package_name..." + if has_cmd apt-get; then + sudo apt-get update + sudo apt-get install -y "$package_name" + elif has_cmd dnf; then + sudo dnf install -y "$package_name" + elif has_cmd yum; then + sudo yum install -y "$package_name" + else + error "Could not find apt-get, dnf, or yum. Please install '$package_name' manually." + return 1 + fi + success "$package_name installed successfully." +} + setup_development_environment() { section "system requirements check" step "verifying required system packages" has_cmd python3 || error "Python 3 is not installed. Install Python 3.12+." success "Python $(python3 --version | cut -d' ' -f2) found." - has_cmd pip || error "pip is not installed." - success "pip $(pip --version | cut -d' ' -f2) found." + + # Check for python3.12-venv and install if missing + if python3.12 -c "import venv" &>/dev/null; then + success "python3.12-venv is available." + else + warn "python3.12-venv module not found." + install_package "python3.12-venv" + fi + + # Check for pip and install if missing + if has_cmd pip; then + success "pip $(pip --version | cut -d' ' -f2) found." + else + warn "pip not found." + install_package "python3-pip" + fi + has_cmd git || error "git is not installed." success "git $(git --version | cut -d' ' -f3) found." + + if has_cmd direnv; then + success "direnv found." + else + warn "direnv not found." + install_package "direnv" + fi + if has_cmd curl; then success "curl available."; else warn "curl not found. Some optional steps may be skipped."; fi + section "virtual environment setup" step "creating Python virtual environment" if [ -d "$VENV_DIR" ]; then From eacc35f79025875013a4e0baf23fb9a0af3f5852 Mon Sep 17 00:00:00 2001 From: romantechletes Date: Fri, 19 Sep 2025 09:52:19 +0200 Subject: [PATCH 056/199] readme's: SETUP.md, USAGE.md have been created to split based on project stage. pre-commit hook troubleshooting is added to the usage readme. --- SETUP.md | 126 +++++++++++++++++++++++++++++++++++++++++++++++ USAGE.md | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 SETUP.md create mode 100644 USAGE.md diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 00000000..58869961 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,126 @@ + +### ๐Ÿ“„ `SETUP.md` - Project Setup Guide + +This guide will walk you through setting up the `python_basic_template` development environment on your local machine. + +## ๐Ÿš€ Quick Start: One-Command Setup + +These scripts automate the entire setup process, including dependency installation, virtual environment creation, and tool configuration. + +### Prerequisites + +Before you begin, ensure you have the following installed: + +* **Python 3.12 or later** +* **Git** +* **1Password CLI**: Required for secret management. Follow the [official installation guide](https://developer.1password.com/docs/cli/get-started/). +* **cURL**: Usually pre-installed on Linux/macOS. +* **Sudo/Administrator access**: For installing system packages like `direnv`. + +--- + +### Linux / macOS Setup + +Open your terminal and run the following commands: + +```bash +# 1. Clone the repository +git clone https://github.com/thom-techlete/python_basic_template.git + +# 2. Navigate into the project directory +cd python_basic_template + +# 3. Run the setup script +./scripts/setup.sh``` + +--- + +### Windows Setup + +Open PowerShell and run the following commands: + +```powershell +# 1. Clone the repository +git clone https://github.com/thom-techlete/python_basic_template.git + +# 2. Navigate into the project directory +cd python_basic_template + +# 3. Run the setup script +.\scripts\setup.ps1 +``` + +After the script finishes, **restart your terminal or PowerShell session** to ensure all environment changes are applied correctly. + +## ๐Ÿค” What the Setup Script Does + +The setup script is designed to create a consistent and reproducible development environment by automating the following steps: + +1. **System Checks**: Verifies that Python, Git, and other required tools are installed. +2. **Virtual Environment**: Creates an isolated Python virtual environment in a `venv/` directory. +3. **Dependency Installation**: Installs all production and development dependencies using `pip-tools` for pinned, reproducible builds. +4. **Pre-Commit Hooks**: Sets up automated code quality checks that run before every commit. +5. **Secret Management**: Initializes `detect-secrets` to prevent accidental credential exposure and prepares for 1Password integration. +6. **Environment Variables**: Configures `direnv` (on Linux/macOS) to automatically manage environment variables when you enter the project directory. + +## ๐Ÿ›Ÿ Common Setup Issues + +
+Windows: PowerShell script execution is blocked + +If you see an error like `File .\setup.ps1 cannot be loaded... not digitally signed`, your system is blocking unsigned PowerShell scripts. + +**Solution:** +1. Open a new PowerShell window **as your normal user** (not as Administrator). +2. Run the following command to allow local scripts to run: + ```powershell + Set-ExecutionPolicy -Scope CurrentUser RemoteSigned + ``` +3. Close the PowerShell window and re-run the setup script in a new one. + +
+ +
+Linux: `python3.12-venv` not found + +On some Linux distributions (like Debian/Ubuntu), the `venv` module is a separate package. The setup script will attempt to install it automatically. + +**Solution:** +If the automatic installation fails, you can install it manually: +```bash +sudo apt-get update +sudo apt-get install python3.12-venv +``` +Then, re-run `./scripts/setup.sh`. +
+ +
+1Password CLI: Not signed in or not linked to WSL + +The setup script may warn that the `op` command-line tool is not signed in. + +**Solution:** +1. **Sign In**: Run `op account list`. If you see "No accounts configured," run `op signin` and follow the prompts. +2. **WSL Users**: If you use the 1Password desktop app on Windows, you must enable WSL integration in the app's settings: `Settings > Developer > Integrate with Windows Subsystem for Linux`. + +
+ +
+Virtual Environment is Corrupted + +If you encounter strange dependency or pathing issues, your virtual environment might be in a bad state. + +**Solution:** +1. Delete the existing virtual environment directory. + ```bash + # On Linux/macOS + rm -rf venv + + # On Windows + Remove-Item -Recurse -Force venv + ``` +2. Re-run the setup script (`./scripts/setup.sh` or `.\scripts\setup.ps1`) to create a fresh one. +
+ +*** + diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 00000000..eea3ebb0 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,145 @@ +### ๐Ÿ“„ `USAGE.md` - Developer Usage Guide + +This guide covers the day-to-day development workflow, including dependency management, code quality checks, and troubleshooting for pre-commit hooks. + +## โš™๏ธ Daily Development Workflow + +1. **Activate Environment**: Navigate to the project directory. If you configured `direnv` during setup, the environment will activate automatically. Otherwise, activate it manually: + ```bash + # Linux / macOS + source venv/bin/activate + + # Windows + .\venv\Scripts\Activate.ps1 + ``` + +2. **Code**: Create or modify your Python modules, notebooks, and documentation. + +3. **Run Quality Checks (Optional)**: Before committing, you can run the same checks that the pre-commit hooks use: + ```bash + # Format code + black . + + # Lint for errors and sort imports + ruff check . --fix + + # Check static types + mypy . + ``` + +4. **Run Tests**: Ensure all existing functionality still works. + ```bash + # Run all tests + pytest + + # Run tests with a coverage report + pytest --cov + ``` + +5. **Commit Code**: Stage your files and commit them. + ```bash + git add . + git commit -m "Your descriptive commit message" + ``` + At this point, the pre-commit hooks will run automatically. See the troubleshooting guide below if any checks fail. + +## ๐Ÿช Understanding and Troubleshooting Pre-Commit Hooks + +Pre-commit hooks are automated checks that run on your code *before* a commit is finalized. They enforce code quality, consistency, and security, preventing common mistakes from entering the codebase. + +If a hook fails, your commit will be aborted. **This is not an error with your code's logic**, but rather an issue with its formatting, style, or safety. + +### General Fix Process + +1. **Read the Error**: The hook will print a message explaining what failed. +2. **Check for Auto-Fixes**: Many hooks (like `black` and `ruff`) will automatically fix the files for you. +3. **Re-stage the Changes**: If files were modified, you must stage them again with `git add .`. +4. **Commit Again**: `git commit ...` + +--- + +### Per-Hook Troubleshooting Guide + +
+Hook: `black` (Code Formatter) + +* **Purpose**: Ensures all Python code follows the same formatting rules. +* **Common Error**: `file would be reformatted` +* **How to Fix**: + 1. The `black` hook has already reformatted the file(s) for you. + 2. Stage the changes made by the hook: `git add .` + 3. Try to commit again. The hook will now pass. + *To run manually*: `black .` + +
+ +
+Hook: `ruff` (Linter & Importer Sorter) + +* **Purpose**: Catches common errors, enforces best practices, and organizes `import` statements. +* **Common Error**: May show various messages, like `import statements are not sorted` or `line too long`. +* **How to Fix**: + 1. Ruff will attempt to auto-fix many issues. Like `black`, you just need to re-stage the changes it made: `git add .` + 2. For issues it cannot fix automatically, the error message will specify the file, line number, and rule (e.g., `F841: unused variable 'x'`). Edit the file to resolve the issue. + 3. Try to commit again. + *To run manually*: `ruff check . --fix` + +
+ +
+Hook: `mypy` (Static Type Checker) + +* **Purpose**: Finds type errors in your code (e.g., passing a `string` to a function that expects an `integer`). +* **Common Error**: `Incompatible types in assignment...` or `Argument 1 to "function" has incompatible type...` +* **How to Fix**: + 1. This hook **does not** auto-fix code. + 2. Read the error message carefully. It will point to the exact file and line where the type mismatch occurred. + 3. Correct your Python code or type annotations to resolve the error. + 4. Stage your changes (`git add .`) and try to commit again. + *To run manually*: `mypy .` + +
+ +
+Hook: `detect-secrets` (Secret Scanner) + +* **Purpose**: Prevents you from accidentally committing secrets like API keys or passwords. +* **Common Error**: `Secrets detected!` followed by a list of findings. +* **How to Fix**: + 1. **NEVER IGNORE THIS HOOK** unless you are 100% certain it is a false positive. + 2. Remove the detected secret from your code. Use the project's secret management system instead (see `docs/1_secret_management.md`). + 3. **For False Positives**: If the detected string is not a secret, you must update the baseline file to tell `detect-secrets` to ignore it in the future: + ```bash + # Re-scan the project and overwrite the baseline + detect-secrets scan > .secrets.baseline + + # Add the new baseline to your staged files + git add .secrets.baseline + ``` + 4. Commit again. + +
+ +
+Hook: `nbstripout` (Jupyter Notebook Cleaner) + +* **Purpose**: Removes large output cells from Jupyter Notebooks (`.ipynb` files) to keep the repository clean and small. +* **Common Error**: `file would be stripped` +* **How to Fix**: + 1. This hook works just like `black`. It has already cleaned the notebook file. + 2. Stage the cleaned notebook: `git add path/to/your/notebook.ipynb` + 3. Try to commit again. + +
+ +## ๐Ÿ—๏ธ Managing Dependencies + +This project uses a hybrid approach where dependencies are declared in `pyproject.toml` and locked to `requirements.txt` files for reproducibility. + +* **To Add a Production Dependency**: + 1. Add the package name (e.g., `"pandas"`) to the `dependencies` list in `pyproject.toml`. + 2. Run the dependency script to update the lockfile: `./scripts/dependency.sh` + +* **To Add a Development-Only Dependency**: + 1. Add the package name (e.g., `"ipykernel"`) to the `dev` list under `[project.optional-dependencies]` in `pyproject.toml`. + 2. Run the dependency script: `./scripts/dependency.sh` \ No newline at end of file From 26a9a24b4bab8d16a479f7caa5e73bf8ca723dd2 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 23 Sep 2025 09:02:53 +0200 Subject: [PATCH 057/199] Update VSCode type checking strictness --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e5a1cafb..a8737fc5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "python.analysis.typeCheckingMode": "strict", + "python.analysis.typeCheckingMode": "standard", "python.linting.mypyEnabled": true, "python.linting.enabled": true, "python.linting.mypyArgs": ["--config-file=pyproject.toml"], From cdf7c11145390f7c427e0e7f7ea882f0d852fabf Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 23 Sep 2025 09:43:53 +0200 Subject: [PATCH 058/199] Update pre commit config Remove old verify lockfile hooks --- .pre-commit-config.yaml | 10 +++++----- scripts/hooks/verify-dev-lockfile.sh | 3 --- scripts/hooks/verify-lockfile.sh | 0 scripts/hooks/verify-prod-lockfile.sh | 3 --- scripts/setup.sh | 18 ------------------ 5 files changed, 5 insertions(+), 29 deletions(-) delete mode 100644 scripts/hooks/verify-dev-lockfile.sh mode change 100644 => 100755 scripts/hooks/verify-lockfile.sh delete mode 100644 scripts/hooks/verify-prod-lockfile.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17f2fa6a..f1d7f82e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: # Verify requirements lockfiles - id: verify-prod-lockfile name: Verify production lockfile is up-to-date - entry: ./scripts/verify-lockfile.sh + entry: ./scripts/hooks/verify-lockfile.sh args: ["requirements.txt"] language: python files: ^requirements\.txt$ @@ -12,7 +12,7 @@ repos: - id: verify-dev-lockfile name: Verify development lockfile is up-to-date - entry: ./scripts/verify-lockfile.sh + entry: ./scripts/hooks/verify-lockfile.sh args: ["requirements-dev.txt"] language: python files: ^requirements-dev\.txt$ @@ -52,14 +52,14 @@ repos: # Notebook/text sync - repo: https://github.com/mwouts/jupytext - rev: v1.17.2 + rev: v1.17.3 hooks: - id: jupytext args: [--sync] require_serial: true - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v1.18.2 hooks: - id: mypy name: Type check with mypy @@ -67,7 +67,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.8 + rev: v0.13.1 hooks: - id: ruff name: Ruff (linting + extra isort + rules) diff --git a/scripts/hooks/verify-dev-lockfile.sh b/scripts/hooks/verify-dev-lockfile.sh deleted file mode 100644 index c6ce08e1..00000000 --- a/scripts/hooks/verify-dev-lockfile.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -"$(dirname "$0")/verify-lockfile.sh" requirements-dev.txt diff --git a/scripts/hooks/verify-lockfile.sh b/scripts/hooks/verify-lockfile.sh old mode 100644 new mode 100755 diff --git a/scripts/hooks/verify-prod-lockfile.sh b/scripts/hooks/verify-prod-lockfile.sh deleted file mode 100644 index 2107b477..00000000 --- a/scripts/hooks/verify-prod-lockfile.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -"$(dirname "$0")/verify-lockfile.sh" requirements.txt diff --git a/scripts/setup.sh b/scripts/setup.sh index 3f51ade0..df2b32ef 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -168,24 +168,6 @@ setup_development_environment() { if pip show "$tool" >/dev/null 2>&1; then success "$tool installed"; else warn "$tool missing"; fi done - section "project validation" - step "running final project validation" - info "checking imports" - python - <<'PYCODE' -import utils.secrets, utils.utils -print("โœ… All project modules import successfully") -PYCODE - info "syntax checking" - find . -name "*.py" -not -path "./$VENV_DIR/*" -exec python -m py_compile {} \; - success "python syntax valid" - if [ -s "$PROD_LOCK" ]; then - info "verifying dependency compatibility" - pip check - success "dependencies compatible" - else - info "no production dependencies to verify" - fi - section "setup complete" echo -e "\n${GREEN}๐ŸŽ‰ development environment setup complete${RESET}\n" echo -e "${BLUE}installed:${RESET}" From 1eae45d66de666aab18ac61f66c0ca8f14e5ea67 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 23 Sep 2025 09:49:56 +0200 Subject: [PATCH 059/199] Update dependencies and improve lockfile verification - Updated `click` from version 8.2.1 to 8.3.0 in `requirements.txt`. - Updated `requests` from version 2.32.4 to 2.32.5 in `requirements.txt`. - Modified `dependency.sh` to include `--strip-extras` flag during lockfile compilation for both production and development environments. - Enhanced `verify-lockfile.sh` to use `--generate-hashes` and `--strip-extras` during lockfile verification, ensuring consistency and integrity checks are more robust. - Added a normalization function in `verify-lockfile.sh` to compare requirements files while ignoring comments and empty lines. --- requirements-dev.txt | 935 ++++++++++++++++--------------- requirements.txt | 14 +- scripts/dependency.sh | 6 +- scripts/hooks/verify-lockfile.sh | 19 +- 4 files changed, 501 insertions(+), 473 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9fd5ddd8..199fd81e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --extra=dev --generate-hashes --output-file=requirements-dev.txt pyproject.toml +# pip-compile --cert=None --client-cert=None --extra=dev --generate-hashes --index-url=None --output-file=requirements-dev.txt --pip-args=None --strip-extras pyproject.toml # attrs==25.3.0 \ --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \ @@ -16,29 +16,29 @@ beartype==0.21.0 \ # via # pytest-beartype # python-basic-template (pyproject.toml) -black==25.1.0 \ - --hash=sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171 \ - --hash=sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7 \ - --hash=sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da \ - --hash=sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2 \ - --hash=sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc \ - --hash=sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666 \ - --hash=sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f \ - --hash=sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b \ - --hash=sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32 \ - --hash=sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f \ - --hash=sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717 \ - --hash=sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299 \ - --hash=sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0 \ - --hash=sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18 \ - --hash=sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0 \ - --hash=sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3 \ - --hash=sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355 \ - --hash=sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096 \ - --hash=sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e \ - --hash=sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9 \ - --hash=sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba \ - --hash=sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f +black==25.9.0 \ + --hash=sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175 \ + --hash=sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619 \ + --hash=sha256:154b06d618233fe468236ba1f0e40823d4eb08b26f5e9261526fde34916b9140 \ + --hash=sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0 \ + --hash=sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92 \ + --hash=sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f \ + --hash=sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa \ + --hash=sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae \ + --hash=sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a \ + --hash=sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357 \ + --hash=sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4 \ + --hash=sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e \ + --hash=sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d \ + --hash=sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608 \ + --hash=sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831 \ + --hash=sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f \ + --hash=sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7 \ + --hash=sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1 \ + --hash=sha256:e3c1f4cd5e93842774d9ee4ef6cd8d17790e65f44f7cdbaab5f2cf8ccf22a823 \ + --hash=sha256:e593466de7b998374ea2585a471ba90553283fb9beefcfa430d84a2651ed5933 \ + --hash=sha256:ef69351df3c84485a8beb6f7b8f9721e2009e20ef80a8d619e2d1788b7816d47 \ + --hash=sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713 # via python-basic-template (pyproject.toml) blinker==1.9.0 \ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ @@ -52,7 +52,7 @@ build==1.3.0 \ --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 # via pip-tools -cachecontrol[filecache]==0.14.3 \ +cachecontrol==0.14.3 \ --hash=sha256:73e7efec4b06b20d9267b441c1f733664f989fb8688391b670ca812d70795d11 \ --hash=sha256:b35e44a3113f17d2a31c1e6b27b9de6d4405f84ae51baa8c1d3cc5b633010cae # via @@ -66,196 +66,199 @@ cfgv==3.4.0 \ --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 # via pre-commit -charset-normalizer==3.4.2 \ - --hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \ - --hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \ - --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \ - --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \ - --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \ - --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \ - --hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \ - --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \ - --hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \ - --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \ - --hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \ - --hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \ - --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \ - --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \ - --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \ - --hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \ - --hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \ - --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \ - --hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \ - --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \ - --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \ - --hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \ - --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \ - --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \ - --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \ - --hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \ - --hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \ - --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \ - --hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \ - --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \ - --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \ - --hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \ - --hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \ - --hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \ - --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \ - --hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \ - --hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \ - --hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \ - --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \ - --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \ - --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \ - --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \ - --hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \ - --hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \ - --hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \ - --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \ - --hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \ - --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \ - --hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \ - --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \ - --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \ - --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \ - --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \ - --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \ - --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \ - --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \ - --hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \ - --hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \ - --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \ - --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \ - --hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \ - --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \ - --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \ - --hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \ - --hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \ - --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \ - --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \ - --hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \ - --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \ - --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \ - --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \ - --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \ - --hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \ - --hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \ - --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \ - --hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \ - --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \ - --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \ - --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \ - --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \ - --hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \ - --hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \ - --hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \ - --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \ - --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \ - --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \ - --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \ - --hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \ - --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \ - --hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \ - --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \ - --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f +charset-normalizer==3.4.3 \ + --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ + --hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \ + --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ + --hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \ + --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ + --hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \ + --hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \ + --hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \ + --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ + --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ + --hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \ + --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ + --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ + --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ + --hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \ + --hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \ + --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ + --hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \ + --hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \ + --hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \ + --hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \ + --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ + --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ + --hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \ + --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ + --hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \ + --hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \ + --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ + --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ + --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ + --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ + --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ + --hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \ + --hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \ + --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ + --hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \ + --hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \ + --hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \ + --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ + --hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \ + --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ + --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ + --hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \ + --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ + --hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \ + --hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \ + --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ + --hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \ + --hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \ + --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ + --hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \ + --hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \ + --hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \ + --hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \ + --hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \ + --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ + --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ + --hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \ + --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ + --hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \ + --hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \ + --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ + --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ + --hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \ + --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ + --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ + --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ + --hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \ + --hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \ + --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ + --hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \ + --hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \ + --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ + --hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \ + --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ + --hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \ + --hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \ + --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ + --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 # via requests -click==8.2.1 \ - --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ - --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b +click==8.3.0 \ + --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ + --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 # via # black # flask # pip-tools -coverage[toml]==7.10.2 \ - --hash=sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b \ - --hash=sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc \ - --hash=sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7 \ - --hash=sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba \ - --hash=sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303 \ - --hash=sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc \ - --hash=sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20 \ - --hash=sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4 \ - --hash=sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01 \ - --hash=sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a \ - --hash=sha256:22aca3e691c7709c5999ccf48b7a8ff5cf5a8bd6fe9b36efbd4993f5a36b2fcf \ - --hash=sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8 \ - --hash=sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27 \ - --hash=sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57 \ - --hash=sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3 \ - --hash=sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c \ - --hash=sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b \ - --hash=sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406 \ - --hash=sha256:44329cbed24966c0b49acb386352c9722219af1f0c80db7f218af7793d251902 \ - --hash=sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468 \ - --hash=sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb \ - --hash=sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed \ - --hash=sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf \ - --hash=sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4 \ - --hash=sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055 \ - --hash=sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b \ - --hash=sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b \ - --hash=sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7 \ - --hash=sha256:6c031da749a05f7a01447dd7f47beedb498edd293e31e1878c0d52db18787df0 \ - --hash=sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074 \ - --hash=sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7 \ - --hash=sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd \ - --hash=sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3 \ - --hash=sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288 \ - --hash=sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0 \ - --hash=sha256:765b13b164685a2f8b2abef867ad07aebedc0e090c757958a186f64e39d63dbd \ - --hash=sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518 \ - --hash=sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65 \ - --hash=sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9 \ - --hash=sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de \ - --hash=sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46 \ - --hash=sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21 \ - --hash=sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83 \ - --hash=sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824 \ - --hash=sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0 \ - --hash=sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f \ - --hash=sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b \ - --hash=sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226 \ - --hash=sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1 \ - --hash=sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0 \ - --hash=sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f \ - --hash=sha256:9bdff88e858ee608a924acfad32a180d2bf6e13e059d6a7174abbae075f30436 \ - --hash=sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be \ - --hash=sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87 \ - --hash=sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1 \ - --hash=sha256:a219b70100500d0c7fd3ebb824a3302efb6b1a122baa9d4eb3f43df8f0b3d899 \ - --hash=sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6 \ - --hash=sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95 \ - --hash=sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0 \ - --hash=sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f \ - --hash=sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186 \ - --hash=sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388 \ - --hash=sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f \ - --hash=sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340 \ - --hash=sha256:bc3945b7bad33957a9eca16e9e5eae4b17cb03173ef594fdaad228f4fc7da53b \ - --hash=sha256:be127f292496d0fbe20d8025f73221b36117b3587f890346e80a13b310712982 \ - --hash=sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322 \ - --hash=sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1 \ - --hash=sha256:c7195444b932356055a8e287fa910bf9753a84a1bc33aeb3770e8fca521e032e \ - --hash=sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0 \ - --hash=sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8 \ - --hash=sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca \ - --hash=sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293 \ - --hash=sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1 \ - --hash=sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9 \ - --hash=sha256:e33e79a219105aa315439ee051bd50b6caa705dc4164a5aba6932c8ac3ce2d98 \ - --hash=sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8 \ - --hash=sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2 \ - --hash=sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e \ - --hash=sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b \ - --hash=sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75 \ - --hash=sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca \ - --hash=sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8 \ - --hash=sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03 \ - --hash=sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce \ - --hash=sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe \ - --hash=sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947 \ - --hash=sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb +coverage==7.10.7 \ + --hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \ + --hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \ + --hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \ + --hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \ + --hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \ + --hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \ + --hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \ + --hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \ + --hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \ + --hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \ + --hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \ + --hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \ + --hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \ + --hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \ + --hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \ + --hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \ + --hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \ + --hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \ + --hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \ + --hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \ + --hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \ + --hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \ + --hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \ + --hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \ + --hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \ + --hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \ + --hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \ + --hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \ + --hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \ + --hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \ + --hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \ + --hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \ + --hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \ + --hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \ + --hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \ + --hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \ + --hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \ + --hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \ + --hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \ + --hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \ + --hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \ + --hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \ + --hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \ + --hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \ + --hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \ + --hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \ + --hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \ + --hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \ + --hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \ + --hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \ + --hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \ + --hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \ + --hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \ + --hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \ + --hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \ + --hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \ + --hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \ + --hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \ + --hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \ + --hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \ + --hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \ + --hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \ + --hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \ + --hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \ + --hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \ + --hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \ + --hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \ + --hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \ + --hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \ + --hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \ + --hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \ + --hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \ + --hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \ + --hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \ + --hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \ + --hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \ + --hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \ + --hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \ + --hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \ + --hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \ + --hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \ + --hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \ + --hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \ + --hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \ + --hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \ + --hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \ + --hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \ + --hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \ + --hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \ + --hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \ + --hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \ + --hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \ + --hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \ + --hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \ + --hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \ + --hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \ + --hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \ + --hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \ + --hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \ + --hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \ + --hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \ + --hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \ + --hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \ + --hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3 # via pytest-cov cyclonedx-python-lib==9.1.0 \ --hash=sha256:55693fca8edaecc3363b24af14e82cc6e659eb1e8353e58b587c42652ce0fb52 \ @@ -273,13 +276,13 @@ distlib==0.4.0 \ --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d # via virtualenv -fastjsonschema==2.21.1 \ - --hash=sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4 \ - --hash=sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667 +fastjsonschema==2.21.2 \ + --hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \ + --hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de # via nbformat -filelock==3.18.0 \ - --hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \ - --hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de +filelock==3.19.1 \ + --hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \ + --hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d # via # cachecontrol # virtualenv @@ -287,9 +290,9 @@ flask==2.3.3 \ --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b # via python-basic-template (pyproject.toml) -identify==2.6.12 \ - --hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \ - --hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6 +identify==2.6.14 \ + --hash=sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e \ + --hash=sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a # via pre-commit idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ @@ -307,13 +310,13 @@ jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via flask -jsonschema==4.25.0 \ - --hash=sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716 \ - --hash=sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f +jsonschema==4.25.1 \ + --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ + --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 # via nbformat -jsonschema-specifications==2025.4.1 \ - --hash=sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af \ - --hash=sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608 +jsonschema-specifications==2025.9.1 \ + --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ + --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema jupyter-core==5.8.1 \ --hash=sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941 \ @@ -323,9 +326,9 @@ license-expression==30.4.4 \ --hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \ --hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd # via cyclonedx-python-lib -markdown-it-py==3.0.0 \ - --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ - --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb +markdown-it-py==4.0.0 \ + --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ + --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 # via rich markupsafe==3.0.2 \ --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \ @@ -457,45 +460,45 @@ msgpack==1.1.1 \ --hash=sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b \ --hash=sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75 # via cachecontrol -mypy==1.17.1 \ - --hash=sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341 \ - --hash=sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5 \ - --hash=sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849 \ - --hash=sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733 \ - --hash=sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81 \ - --hash=sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403 \ - --hash=sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6 \ - --hash=sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01 \ - --hash=sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91 \ - --hash=sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972 \ - --hash=sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8 \ - --hash=sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd \ - --hash=sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9 \ - --hash=sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0 \ - --hash=sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19 \ - --hash=sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb \ - --hash=sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd \ - --hash=sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99 \ - --hash=sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7 \ - --hash=sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056 \ - --hash=sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7 \ - --hash=sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a \ - --hash=sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed \ - --hash=sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94 \ - --hash=sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9 \ - --hash=sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58 \ - --hash=sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8 \ - --hash=sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5 \ - --hash=sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a \ - --hash=sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df \ - --hash=sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb \ - --hash=sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d \ - --hash=sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390 \ - --hash=sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b \ - --hash=sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b \ - --hash=sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14 \ - --hash=sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259 \ - --hash=sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b +mypy==1.18.2 \ + --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ + --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ + --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ + --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ + --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ + --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ + --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ + --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ + --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ + --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ + --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ + --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ + --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ + --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ + --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ + --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ + --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ + --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ + --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ + --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ + --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ + --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ + --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ + --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ + --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ + --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ + --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ + --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ + --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ + --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ + --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ + --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ + --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ + --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ + --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ + --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ + --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ + --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 # via python-basic-template (pyproject.toml) mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ @@ -515,9 +518,9 @@ nodeenv==1.9.1 \ --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 # via pre-commit -packageurl-python==0.17.3 \ - --hash=sha256:719995f0c7f706890277ba57ec95afcaa9696c836a7675770a1279b01a41f7be \ - --hash=sha256:f51b5aab570159f07258c8e998e9972ff3bf060da16b7334a42bd9f9737777d9 +packageurl-python==0.17.5 \ + --hash=sha256:a7be3f3ba70d705f738ace9bf6124f31920245a49fa69d4b416da7037dd2de61 \ + --hash=sha256:f0e55452ab37b5c192c443de1458e3f3b4d8ac27f747df6e8c48adeab081d321 # via cyclonedx-python-lib packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ @@ -550,9 +553,9 @@ pip-tools==7.5.0 \ --hash=sha256:30639f50961bb09f49d22f4389e8d7d990709677c094ce1114186b1f2e9b5821 \ --hash=sha256:69758e4e5a65f160e315d74db46246fdbb30d549f1ed0c4236d057122c9b0f18 # via python-basic-template (pyproject.toml) -platformdirs==4.3.8 \ - --hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \ - --hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4 +platformdirs==4.4.0 \ + --hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \ + --hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf # via # black # jupyter-core @@ -564,9 +567,9 @@ pluggy==1.6.0 \ # via # pytest # pytest-cov -pre-commit==4.2.0 \ - --hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \ - --hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd +pre-commit==4.3.0 \ + --hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \ + --hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16 # via python-basic-template (pyproject.toml) py-serializable==2.1.0 \ --hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \ @@ -578,9 +581,9 @@ pygments==2.19.2 \ # via # pytest # rich -pyparsing==3.2.3 \ - --hash=sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf \ - --hash=sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be +pyparsing==3.2.5 \ + --hash=sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6 \ + --hash=sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e # via pip-requirements-parser pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ @@ -588,9 +591,9 @@ pyproject-hooks==1.2.0 \ # via # build # pip-tools -pytest==8.4.1 \ - --hash=sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7 \ - --hash=sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c +pytest==8.4.2 \ + --hash=sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01 \ + --hash=sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79 # via # pytest-beartype # pytest-cov @@ -599,10 +602,14 @@ pytest-beartype==0.2.0 \ --hash=sha256:9eeec379f812e0e5c9c1bb7e95d90a33a79c387034f2d735e03bc9aa0fd7cf3f \ --hash=sha256:be6be04be4a143f965f21140d6661bab43dcd7c761d50cf6daa1b16848e5d2d0 # via python-basic-template (pyproject.toml) -pytest-cov==6.2.1 \ - --hash=sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2 \ - --hash=sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5 +pytest-cov==7.0.0 \ + --hash=sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1 \ + --hash=sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861 # via python-basic-template (pyproject.toml) +pytokens==0.1.10 \ + --hash=sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044 \ + --hash=sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b + # via black pyyaml==6.0.2 \ --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ @@ -666,9 +673,9 @@ referencing==0.36.2 \ # via # jsonschema # jsonschema-specifications -requests==2.32.4 \ - --hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \ - --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422 +requests==2.32.5 \ + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf # via # cachecontrol # detect-secrets @@ -680,173 +687,185 @@ rich==14.1.0 \ # via # pip-audit # python-basic-template (pyproject.toml) -rpds-py==0.26.0 \ - --hash=sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3 \ - --hash=sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b \ - --hash=sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5 \ - --hash=sha256:0c71c2f6bf36e61ee5c47b2b9b5d47e4d1baad6426bfed9eea3e858fc6ee8806 \ - --hash=sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e \ - --hash=sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4 \ - --hash=sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f \ - --hash=sha256:1533b7eb683fb5f38c1d68a3c78f5fdd8f1412fa6b9bf03b40f450785a0ab915 \ - --hash=sha256:1766b5724c3f779317d5321664a343c07773c8c5fd1532e4039e6cc7d1a815be \ - --hash=sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b \ - --hash=sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696 \ - --hash=sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323 \ - --hash=sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331 \ - --hash=sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8 \ - --hash=sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c \ - --hash=sha256:1d815d48b1804ed7867b539236b6dd62997850ca1c91cad187f2ddb1b7bbef19 \ - --hash=sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1 \ - --hash=sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8 \ - --hash=sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0 \ - --hash=sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318 \ - --hash=sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246 \ - --hash=sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256 \ - --hash=sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b \ - --hash=sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb \ - --hash=sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04 \ - --hash=sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1 \ - --hash=sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19 \ - --hash=sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7 \ - --hash=sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f \ - --hash=sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871 \ - --hash=sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84 \ - --hash=sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e \ - --hash=sha256:39bfea47c375f379d8e87ab4bb9eb2c836e4f2069f0f65731d85e55d74666387 \ - --hash=sha256:3ac51b65e8dc76cf4949419c54c5528adb24fc721df722fd452e5fbc236f5c40 \ - --hash=sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958 \ - --hash=sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44 \ - --hash=sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582 \ - --hash=sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7 \ - --hash=sha256:43f10b007033f359bc3fa9cd5e6c1e76723f056ffa9a6b5c117cc35720a80292 \ - --hash=sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a \ - --hash=sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba \ - --hash=sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953 \ - --hash=sha256:4b1f66eb81eab2e0ff5775a3a312e5e2e16bf758f7b06be82fb0d04078c7ac51 \ - --hash=sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9 \ - --hash=sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37 \ - --hash=sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9 \ - --hash=sha256:4f01a5d6444a3258b00dc07b6ea4733e26f8072b788bef750baa37b370266137 \ - --hash=sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3 \ - --hash=sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0 \ - --hash=sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8 \ - --hash=sha256:519067e29f67b5c90e64fb1a6b6e9d2ec0ba28705c51956637bac23a2f4ddae1 \ - --hash=sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e \ - --hash=sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618 \ - --hash=sha256:582462833ba7cee52e968b0341b85e392ae53d44c0f9af6a5927c80e539a8b67 \ - --hash=sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1 \ - --hash=sha256:59b2093224a18c6508d95cfdeba8db9cbfd6f3494e94793b58972933fcee4c6d \ - --hash=sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9 \ - --hash=sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da \ - --hash=sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0 \ - --hash=sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b \ - --hash=sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84 \ - --hash=sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d \ - --hash=sha256:69a607203441e07e9a8a529cff1d5b73f6a160f22db1097211e6212a68567d11 \ - --hash=sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1 \ - --hash=sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7 \ - --hash=sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79 \ - --hash=sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f \ - --hash=sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88 \ - --hash=sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0 \ - --hash=sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a \ - --hash=sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158 \ - --hash=sha256:7a48af25d9b3c15684059d0d1fc0bc30e8eee5ca521030e2bffddcab5be40226 \ - --hash=sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f \ - --hash=sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f \ - --hash=sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1 \ - --hash=sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad \ - --hash=sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7 \ - --hash=sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a \ - --hash=sha256:84cfbd4d4d2cdeb2be61a057a258d26b22877266dd905809e94172dff01a42ae \ - --hash=sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08 \ - --hash=sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03 \ - --hash=sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a \ - --hash=sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d \ - --hash=sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11 \ - --hash=sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6 \ - --hash=sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9 \ - --hash=sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb \ - --hash=sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca \ - --hash=sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf \ - --hash=sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9 \ - --hash=sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15 \ - --hash=sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19 \ - --hash=sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed \ - --hash=sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed \ - --hash=sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6 \ - --hash=sha256:a90a13408a7a856b87be8a9f008fff53c5080eea4e4180f6c2e546e4a972fb5d \ - --hash=sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387 \ - --hash=sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f \ - --hash=sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8 \ - --hash=sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5 \ - --hash=sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37 \ - --hash=sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45 \ - --hash=sha256:b5f7a446ddaf6ca0fad9a5535b56fbfc29998bf0e0b450d174bbec0d600e1d72 \ - --hash=sha256:b6d9e5a2ed9c4988c8f9b28b3bc0e3e5b1aaa10c28d210a594ff3a8c02742daf \ - --hash=sha256:b6e2c12160c72aeda9d1283e612f68804621f448145a210f1bf1d79151c47090 \ - --hash=sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20 \ - --hash=sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e \ - --hash=sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e \ - --hash=sha256:c5ab0ee51f560d179b057555b4f601b7df909ed31312d301b99f8b9fc6028284 \ - --hash=sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc \ - --hash=sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8 \ - --hash=sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867 \ - --hash=sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33 \ - --hash=sha256:cb28c1f569f8d33b2b5dcd05d0e6ef7005d8639c54c2f0be824f05aedf715255 \ - --hash=sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8 \ - --hash=sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c \ - --hash=sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323 \ - --hash=sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107 \ - --hash=sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d \ - --hash=sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a \ - --hash=sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2 \ - --hash=sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0 \ - --hash=sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41 \ - --hash=sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af \ - --hash=sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d \ - --hash=sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632 \ - --hash=sha256:e3730a48e5622e598293eee0762b09cff34dd3f271530f47b0894891281f051d \ - --hash=sha256:e5162afc9e0d1f9cae3b577d9c29ddbab3505ab39012cb794d94a005825bde21 \ - --hash=sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170 \ - --hash=sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c \ - --hash=sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf \ - --hash=sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd \ - --hash=sha256:eed5ac260dd545fbc20da5f4f15e7efe36a55e0e7cf706e4ec005b491a9546a0 \ - --hash=sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7 \ - --hash=sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3 \ - --hash=sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35 \ - --hash=sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674 \ - --hash=sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73 \ - --hash=sha256:fbaa70553ca116c77717f513e08815aec458e6b69a028d4028d403b3bc84ff37 \ - --hash=sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f \ - --hash=sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136 \ - --hash=sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83 \ - --hash=sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12 \ - --hash=sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9 +rpds-py==0.27.1 \ + --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ + --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ + --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ + --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ + --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ + --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ + --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ + --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ + --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ + --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ + --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ + --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ + --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ + --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ + --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ + --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ + --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ + --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ + --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ + --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ + --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ + --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ + --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ + --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ + --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ + --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ + --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ + --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ + --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ + --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ + --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ + --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ + --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ + --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ + --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ + --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ + --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ + --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ + --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ + --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ + --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ + --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ + --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ + --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ + --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ + --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ + --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ + --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ + --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ + --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ + --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ + --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ + --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ + --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ + --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ + --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ + --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ + --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ + --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ + --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ + --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ + --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ + --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ + --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ + --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ + --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ + --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ + --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ + --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ + --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ + --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ + --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ + --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ + --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ + --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ + --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ + --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ + --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ + --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ + --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ + --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ + --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ + --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ + --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ + --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ + --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ + --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ + --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ + --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ + --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ + --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ + --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ + --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ + --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ + --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ + --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ + --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ + --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ + --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ + --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ + --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ + --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ + --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ + --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ + --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ + --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ + --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ + --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ + --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ + --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ + --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ + --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ + --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ + --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ + --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ + --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ + --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ + --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ + --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ + --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ + --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ + --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ + --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ + --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ + --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ + --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ + --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ + --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ + --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ + --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ + --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ + --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ + --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ + --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ + --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ + --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ + --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ + --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ + --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ + --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ + --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ + --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ + --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ + --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ + --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ + --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ + --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ + --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ + --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ + --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ + --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ + --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ + --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ + --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ + --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 # via # jsonschema # referencing -ruff==0.12.7 \ - --hash=sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606 \ - --hash=sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71 \ - --hash=sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3 \ - --hash=sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860 \ - --hash=sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa \ - --hash=sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c \ - --hash=sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb \ - --hash=sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e \ - --hash=sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5 \ - --hash=sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd \ - --hash=sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303 \ - --hash=sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb \ - --hash=sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f \ - --hash=sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4 \ - --hash=sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423 \ - --hash=sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77 \ - --hash=sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69 \ - --hash=sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8 +ruff==0.13.1 \ + --hash=sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5 \ + --hash=sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700 \ + --hash=sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334 \ + --hash=sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0 \ + --hash=sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae \ + --hash=sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e \ + --hash=sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51 \ + --hash=sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a \ + --hash=sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0 \ + --hash=sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a \ + --hash=sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b \ + --hash=sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a \ + --hash=sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36 \ + --hash=sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c \ + --hash=sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae \ + --hash=sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317 \ + --hash=sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38 \ + --hash=sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783 \ + --hash=sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389 # via python-basic-template (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ @@ -862,9 +881,9 @@ traitlets==5.14.3 \ # via # jupyter-core # nbformat -typing-extensions==4.14.1 \ - --hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \ - --hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76 +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # mypy # referencing @@ -872,9 +891,9 @@ urllib3==2.5.0 \ --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc # via requests -virtualenv==20.33.0 \ - --hash=sha256:106b6baa8ab1b526d5a9b71165c85c456fbd49b16976c88e2bc9352ee3bc5d3f \ - --hash=sha256:47e0c0d2ef1801fce721708ccdf2a28b9403fa2307c3268aebd03225976f61d2 +virtualenv==20.34.0 \ + --hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \ + --hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a # via pre-commit werkzeug==3.1.3 \ --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ diff --git a/requirements.txt b/requirements.txt index d2be873c..bc7ab5db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --generate-hashes --output-file=requirements.txt pyproject.toml +# pip-compile --cert=None --client-cert=None --generate-hashes --index-url=None --output-file=requirements.txt --pip-args=None --strip-extras pyproject.toml # blinker==1.9.0 \ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ @@ -93,9 +93,9 @@ charset-normalizer==3.4.3 \ --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 # via requests -click==8.2.1 \ - --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ - --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b +click==8.3.0 \ + --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ + --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 # via flask flask==2.3.3 \ --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ @@ -178,9 +178,9 @@ markupsafe==3.0.2 \ # via # jinja2 # werkzeug -requests==2.32.4 \ - --hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \ - --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422 +requests==2.32.5 \ + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf # via python-basic-template (pyproject.toml) urllib3==2.5.0 \ --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ diff --git a/scripts/dependency.sh b/scripts/dependency.sh index ac1a1492..50c69322 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -100,7 +100,8 @@ if [ "$INSTALL_PROD" = true ]; then info "Compiling production lockfile: '${PROD_LOCK}'" pip-compile "${PYPROJECT}" \ --output-file="${PROD_LOCK}" \ - --generate-hashes + --generate-hashes \ + --strip-extras fi if [ "$INSTALL_DEV" = true ]; then @@ -108,7 +109,8 @@ if [ "$INSTALL_DEV" = true ]; then pip-compile "${PYPROJECT}" \ --extra=dev \ --output-file="${DEV_LOCK}" \ - --generate-hashes + --generate-hashes \ + --strip-extras fi success "All required lockfiles compiled." diff --git a/scripts/hooks/verify-lockfile.sh b/scripts/hooks/verify-lockfile.sh index 38616028..34fc4ed0 100755 --- a/scripts/hooks/verify-lockfile.sh +++ b/scripts/hooks/verify-lockfile.sh @@ -16,18 +16,25 @@ echo "Verifying integrity of '$LOCKFILE_TO_CHECK'..." # Determine which compile command to run based on the filename if [[ "$LOCKFILE_TO_CHECK" == "requirements-dev.txt" ]]; then # Compile development dependencies - pip-compile "$PYPROJECT_FILE" --extra=dev --output-file="$TEMP_LOCKFILE" > /dev/null + pip-compile "$PYPROJECT_FILE" --extra=dev --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null else # Compile production dependencies by default - pip-compile "$PYPROJECT_FILE" --output-file="$TEMP_LOCKFILE" > /dev/null + pip-compile "$PYPROJECT_FILE" --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null fi -# Compare the officially generated temp file with the one being committed. -# The `diff -q` command is quiet and exits with a non-zero status if files differ. -if ! diff -q "$LOCKFILE_TO_CHECK" "$TEMP_LOCKFILE"; then +# Function to normalize a requirements file by removing the header comments +# and keeping only the actual dependency lines +normalize_requirements() { + local file="$1" + # Skip lines that start with # and extract only the actual requirements + grep -v '^#' "$file" | grep -v '^$' | sort +} + +# Compare the normalized content (excluding headers) +if ! diff -q <(normalize_requirements "$LOCKFILE_TO_CHECK") <(normalize_requirements "$TEMP_LOCKFILE") > /dev/null; then echo "โŒ Error: '$LOCKFILE_TO_CHECK' has been manually edited or is out of date." >&2 echo " It does not match the expected output based on '$PYPROJECT_FILE'." >&2 - echo " To fix this, run 'rm requirements.txt' and then './scripts/dependency.sh' and commit the resulting changes." >&2 + echo " To fix this, run 'rm $LOCKFILE_TO_CHECK' and then './scripts/dependency.sh' and commit the resulting changes." >&2 rm "$TEMP_LOCKFILE" # Always clean up exit 1 fi From 4adef794f07de6b616012255761e24587cfd1e14 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 23 Sep 2025 10:13:23 +0200 Subject: [PATCH 060/199] Improve virtual environment activation in precommit script --- scripts/hooks/precommit_pytest.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/hooks/precommit_pytest.sh b/scripts/hooks/precommit_pytest.sh index dabee339..be66054f 100755 --- a/scripts/hooks/precommit_pytest.sh +++ b/scripts/hooks/precommit_pytest.sh @@ -1,8 +1,15 @@ #!/usr/bin/env bash set -e -# Activate your local venv (adjust path if needed) -source venv/bin/activate +# Activate your local venv (.venv preferred, fallback to venv) +if [ -d ".venv" ]; then + source .venv/bin/activate +elif [ -d "venv" ]; then + source venv/bin/activate +else + echo "No virtual environment found (.venv or venv)." + exit 1 +fi # Install your project in editable mode with dev extras pip install -q -e .[dev] From b91fdfc91ade5c0c0663094b74c153003ad04839 Mon Sep 17 00:00:00 2001 From: divaanster Date: Wed, 24 Sep 2025 10:46:20 +0200 Subject: [PATCH 061/199] minor addition to setup.md --- SETUP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SETUP.md b/SETUP.md index 58869961..d511fe08 100644 --- a/SETUP.md +++ b/SETUP.md @@ -47,6 +47,7 @@ git clone https://github.com/thom-techlete/python_basic_template.git cd python_basic_template # 3. Run the setup script +# If activation is blocked: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned .\scripts\setup.ps1 ``` From 7534e1c8fd168d7979a371b9078b5bcf2405b2e5 Mon Sep 17 00:00:00 2001 From: divaanster Date: Wed, 24 Sep 2025 11:14:10 +0200 Subject: [PATCH 062/199] updates setup.sh --- scripts/setup.sh | 119 ++++++++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 43 deletions(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 42f799ab..ba231a68 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -32,11 +32,15 @@ is_wsl() { has_cmd() { command -v "$1" >/dev/null 2>&1; } # Universal package installation function +APT_UPDATED="" install_package() { local package_name=$1 step "Attempting to install $package_name..." if has_cmd apt-get; then - sudo apt-get update + if [[ -z "${APT_UPDATED}" ]]; then + sudo apt-get update + APT_UPDATED="1" + fi sudo apt-get install -y "$package_name" elif has_cmd dnf; then sudo dnf install -y "$package_name" @@ -49,20 +53,33 @@ install_package() { success "$package_name installed successfully." } + setup_development_environment() { section "system requirements check" step "verifying required system packages" has_cmd python3 || error "Python 3 is not installed. Install Python 3.12+." success "Python $(python3 --version | cut -d' ' -f2) found." - # Check for python3.12-venv and install if missing - if python3.12 -c "import venv" &>/dev/null; then - success "python3.12-venv is available." + # Ensure the venv module exists for the active python3 + PY_MINOR="$(python3 -c 'import sys; print(f\"{sys.version_info.major}.{sys.version_info.minor}\")')" + if python3 -c "import venv" &>/dev/null; then + success "python${PY_MINOR} venv module is available." else - warn "python3.12-venv module not found." - install_package "python3.12-venv" + warn "venv module missing for python${PY_MINOR}." + # Best effort install per distro + if has_cmd apt-get; then + install_package "python${PY_MINOR}-venv" || warn "could not install python${PY_MINOR}-venv automatically" + elif has_cmd dnf; then + # On Fedora venv is in the python base package + install_package "python${PY_MINOR}" || true + elif has_cmd yum; then + install_package "python${PY_MINOR}" || true + else + warn "install the appropriate python venv package manually" + fi fi + # Check for pip and install if missing if has_cmd pip; then success "pip $(pip --version | cut -d' ' -f2) found." @@ -88,8 +105,15 @@ setup_development_environment() { step "creating Python virtual environment" if [ -d "$VENV_DIR" ]; then warn "virtual environment already exists" - read -p "Enter 1 to keep, 2 to recreate [default: 2]: " VENV_CHOICE - VENV_CHOICE=${VENV_CHOICE:-2} + if [[ -n "${VENV_CHOICE:-}" ]]; then + info "VENV_CHOICE preset to ${VENV_CHOICE}" + elif [[ "${CI:-}" == "true" || "${NONINTERACTIVE:-}" == "1" ]]; then + VENV_CHOICE=2 + info "non interactive mode active. defaulting to recreate" + else + read -p "Enter 1 to keep, 2 to recreate [default: 2]: " VENV_CHOICE + VENV_CHOICE=${VENV_CHOICE:-2} + fi if [ "$VENV_CHOICE" = "1" ]; then success "keeping existing virtual environment" # shellcheck disable=SC1090 @@ -111,6 +135,7 @@ setup_development_environment() { success "virtual environment created and activated" fi + step "upgrading pip in virtual environment" python -m pip install --upgrade pip success "pip upgraded to $(pip --version | cut -d' ' -f2)." @@ -119,7 +144,7 @@ setup_development_environment() { info "production deps in $PYPROJECT to $PROD_LOCK" info "development deps in $PYPROJECT extra [dev] to $DEV_LOCK" step "installing pip tools" - pip install pip-tools build wheel + python -m pip install pip-tools build wheel success "pip tools installed" step "running dependency setup script" @@ -127,44 +152,47 @@ setup_development_environment() { success "dependencies installed" section "system locale configuration" - if has_cmd apt; then - step "ensuring locales" - sudo apt update - sudo apt install -y locales + step "ensuring locales" + if has_cmd apt-get; then + sudo apt-get update + sudo apt-get install -y locales sudo locale-gen en_US.UTF-8 sudo update-locale LANG=en_US.UTF-8 - success "locales configured" + success "locales configured for Debian family" + elif has_cmd dnf || has_cmd yum; then + # On Fedora and RHEL locales are typically available. Configure via localectl. + if has_cmd localectl; then + sudo localectl set-locale LANG=en_US.UTF-8 + success "locales configured via localectl" + else + warn "localectl not found. verify locale manually" + fi else - warn "apt not available. skipping locale setup" + warn "no known package manager detected for locale configuration" fi + section "direnv setup" - if has_cmd apt; then - step "installing direnv" - sudo apt install -y direnv - success "direnv installed" - step "configuring direnv shell integration" - PROFILE="${HOME}/.bashrc" - if ! grep -q 'direnv hook bash' "$PROFILE"; then - echo 'eval "$(direnv hook bash)"' >> "$PROFILE" - success "added direnv hook to $PROFILE" - else - warn "direnv hook already present in $PROFILE" - fi - step "enabling direnv for this project" - if [ -f ".envrc" ]; then - direnv allow || true - success "direnv enabled for this project" - else - warn ".envrc not found. skipping allow" - fi + step "configuring direnv shell integration" + PROFILE="${HOME}/.bashrc" + if ! grep -q 'direnv hook bash' "$PROFILE"; then + echo 'eval "$(direnv hook bash)"' >> "$PROFILE" + success "added direnv hook to $PROFILE" else - warn "apt not available. install direnv manually if desired" + warn "direnv hook already present in $PROFILE" + fi + step "enabling direnv for this project" + if [ -f ".envrc" ]; then + direnv allow || true + success "direnv enabled for this project" + else + warn ".envrc not found. skipping allow" fi section "pre commit hooks setup" step "installing pre commit hooks" if [[ -f ".pre-commit-config.yaml" ]]; then + python -m pip install pre-commit detect-secrets || true pre-commit install success "pre commit hooks installed" step "updating pre commit repos" @@ -206,27 +234,32 @@ setup_development_environment() { section "development tools verification" step "verifying development tools" for tool in black ruff mypy pytest beartype; do - if pip show "$tool" >/dev/null 2>&1; then success "$tool installed"; else warn "$tool missing"; fi + if python -m pip show "$tool" >/dev/null 2>&1; then success "$tool installed"; else warn "$tool missing"; fi done section "project validation" step "running final project validation" info "checking imports" - python - <<'PYCODE' -import utils.secrets, utils.utils + + # Use the venv python explicitly and fail fast if imports break + "$VENV_DIR/bin/python" - <<'PYCODE' +import importlib +mods = ["utils.secrets", "utils.utils"] +for m in mods: + importlib.import_module(m) print("โœ… All project modules import successfully") PYCODE - info "syntax checking" - find . -name "*.py" -not -path "./$VENV_DIR/*" -exec python -m py_compile {} \; - success "python syntax valid" + + find . -name "*.py" -not -path "./$VENV_DIR/*" -exec "$VENV_DIR/bin/python" -m py_compile {} \; + if [ -s "$PROD_LOCK" ]; then - info "verifying dependency compatibility" - pip check + "$VENV_DIR/bin/python" -m pip check success "dependencies compatible" else info "no production dependencies to verify" fi + section "setup complete" echo -e "\n${GREEN}๐ŸŽ‰ development environment setup complete${RESET}\n" echo -e "${BLUE}installed:${RESET}" From 38c1ca0cf546a951fcb44815f4677cbaabebe1c5 Mon Sep 17 00:00:00 2001 From: divaanster Date: Wed, 24 Sep 2025 11:19:25 +0200 Subject: [PATCH 063/199] updates setup.sh --- scripts/setup.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index ba231a68..3cf4b6ba 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -137,8 +137,9 @@ setup_development_environment() { step "upgrading pip in virtual environment" + python -m ensurepip --upgrade >/dev/null 2>&1 || true python -m pip install --upgrade pip - success "pip upgraded to $(pip --version | cut -d' ' -f2)." + success "pip upgraded to $("$VENV_DIR/bin/python" -m pip --version | awk '{print $2}')." section "hybrid dependency management" info "production deps in $PYPROJECT to $PROD_LOCK" @@ -154,7 +155,6 @@ setup_development_environment() { section "system locale configuration" step "ensuring locales" if has_cmd apt-get; then - sudo apt-get update sudo apt-get install -y locales sudo locale-gen en_US.UTF-8 sudo update-locale LANG=en_US.UTF-8 @@ -221,6 +221,12 @@ setup_development_environment() { error "1Password CLI not found. install and configure before secrets workflow" fi fi + + # Ensure detect-secrets is available before using it + if ! command -v detect-secrets >/dev/null 2>&1; then + python -m pip install detect-secrets + fi + step "initializing secrets baseline" if [ -f ".secrets.baseline" ]; then warn "existing .secrets.baseline found. backing up" @@ -241,7 +247,10 @@ setup_development_environment() { step "running final project validation" info "checking imports" - # Use the venv python explicitly and fail fast if imports break + section "project validation" + step "running final project validation" + info "checking imports" + "$VENV_DIR/bin/python" - <<'PYCODE' import importlib mods = ["utils.secrets", "utils.utils"] From 0ffd3e2850fdd6166e497c0c1653137a0040ea21 Mon Sep 17 00:00:00 2001 From: dominic-techletes Date: Wed, 24 Sep 2025 16:31:35 +0200 Subject: [PATCH 064/199] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 34b35fa2..b97c0468 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co ### Linux: One-Command Setup ```bash -git clone https://github.com/thom-techlete/python_basic_template.git +git clone https://github.com/TECHLETES/{REPO_NAME}.git cd python_basic_template ./scripts/setup.sh ``` @@ -27,7 +27,7 @@ cd python_basic_template ### Windows setup ```powershell -git clone https://github.com/thom-techlete/python_basic_template.git +git clone https://github.com/TECHLETES/{REPO_NAME}.git cd python_basic_template # If activation is blocked: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned .\scripts\setup.ps1 From 6a19c972dd734afe55ba24ca6418441f10989045 Mon Sep 17 00:00:00 2001 From: dominic-techletes Date: Wed, 24 Sep 2025 16:32:07 +0200 Subject: [PATCH 065/199] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b97c0468..de7ce664 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co ```bash git clone https://github.com/TECHLETES/{REPO_NAME}.git -cd python_basic_template +cd {REPO_NAME} ./scripts/setup.sh ``` @@ -28,7 +28,7 @@ cd python_basic_template ```powershell git clone https://github.com/TECHLETES/{REPO_NAME}.git -cd python_basic_template +cd {REPO_NAME} # If activation is blocked: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned .\scripts\setup.ps1 ``` From bcd78e517eb93956029039a04e59893ea0aa33b1 Mon Sep 17 00:00:00 2001 From: dominic-techletes Date: Wed, 24 Sep 2025 16:33:02 +0200 Subject: [PATCH 066/199] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index de7ce664..376a33c7 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ cd {REPO_NAME} ```powershell git clone https://github.com/TECHLETES/{REPO_NAME}.git cd {REPO_NAME} -# If activation is blocked: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned .\scripts\setup.ps1 ``` From f0180ae7fcc924f3a833f2f611c5745fcfeff7e4 Mon Sep 17 00:00:00 2001 From: divaanster Date: Wed, 24 Sep 2025 17:12:31 +0200 Subject: [PATCH 067/199] updates readme --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++++- USAGE.md | 2 +- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 376a33c7..fdb3165e 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,50 @@ A modern, production-ready Python project template for TECHLETES, a data & AI consultancy. Includes hybrid dependency management, secure secret management, and comprehensive development tooling for internal use by TECHLETES employees. -## ๐Ÿš€ Quick Start +## Read this first! + +*This section lists the essential rules for using this template. Read it before you start.* + +### Secrets and environment +* *Never* put secrets in code, notebooks, or `.env` files! +* Load secrets using 1Password with the helper in code + from utils.secrets import get_secret + API_KEY = get_secret("op:////") +* See [docs/1_secret_management.md](docs/1_secret_management.md) for mor info on managing secrets. + +### Dependencies +* *Do not* run `pip install` manually in the terminal! + Why: it creates unpinned installs that differ per user and will break CI +* Always declare packages in pyproject.toml only +* Run the helper script to handle the package install for you + All dependencies + ./scripts/dependency.sh + Production only + ./scripts/dependency.sh --prod + Development only + ./scripts/dependency.sh --dev +* After you change pyproject.toml run the script again. This will update requirements.txt and requirements-dev.txt automatically. + +### Coding +* Prefer small functions and clear modules so code is easy to test and reuse +* Use type hints to show expected inputs and outputs + Example + def add(a: int, b: int) -> int: + return a + b + Why: your editor can catch mistakes early and mypy can check types automatically +* Put shared helpers in utils +* Use jupyter notebooks only for small tests / development. + +### Git and commits +* Pre commit is a tool that runs checks before every commit to keep code safe and consistent. +* Your commit can be blocked if a check fails. This is normal and protects the repo. +* *Always check if your changes have been committed and pushed successfully!* +* You can also run the pre-commit checks manually on all files before committing, using: + `pre-commit run --all-files` +* If a pre commit check fails and you are stuck, see the troubleshooting section in + +## ๐Ÿš€ Quick Start ### Prerequisites diff --git a/USAGE.md b/USAGE.md index eea3ebb0..f9228d61 100644 --- a/USAGE.md +++ b/USAGE.md @@ -10,7 +10,7 @@ This guide covers the day-to-day development workflow, including dependency mana source venv/bin/activate # Windows - .\venv\Scripts\Activate.ps1 + .venv/Scripts/Activate.ps1 ``` 2. **Code**: Create or modify your Python modules, notebooks, and documentation. From ecc9ee64e94d5e140bcbc9c3859e3e7bf0d3e2c2 Mon Sep 17 00:00:00 2001 From: divaanster Date: Wed, 24 Sep 2025 17:18:08 +0200 Subject: [PATCH 068/199] renames docs --- README.md | 10 +++++----- USAGE.md => docs/1_usage.md | 0 .../{1_secret_management.md => 2_secret_management.md} | 0 ...ndency_management.md => 3_dependency_management.md} | 0 docs/{3_pre_commit_hooks.md => 5_pre_commit_hooks.md} | 0 5 files changed, 5 insertions(+), 5 deletions(-) rename USAGE.md => docs/1_usage.md (100%) rename docs/{1_secret_management.md => 2_secret_management.md} (100%) rename docs/{2_dependency_management.md => 3_dependency_management.md} (100%) rename docs/{3_pre_commit_hooks.md => 5_pre_commit_hooks.md} (100%) diff --git a/README.md b/README.md index fdb3165e..599b09d7 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,19 @@ A modern, production-ready Python project template for TECHLETES, a data & AI consultancy. Includes hybrid dependency management, secure secret management, and comprehensive development tooling for internal use by TECHLETES employees. - -## Read this first! +## READ THIS FIRST! *This section lists the essential rules for using this template. Read it before you start.* ### Secrets and environment -* *Never* put secrets in code, notebooks, or `.env` files! +* **Never** put secrets in code, notebooks, or `.env` files! * Load secrets using 1Password with the helper in code from utils.secrets import get_secret API_KEY = get_secret("op:////") * See [docs/1_secret_management.md](docs/1_secret_management.md) for mor info on managing secrets. ### Dependencies -* *Do not* run `pip install` manually in the terminal! +* **Do not** run `pip install` manually in the terminal! Why: it creates unpinned installs that differ per user and will break CI * Always declare packages in pyproject.toml only * Run the helper script to handle the package install for you @@ -42,8 +41,9 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co ### Git and commits * Pre commit is a tool that runs checks before every commit to keep code safe and consistent. +* **Important:** The pre-commit checks require certain packages that are installed in the virtual environment when you setup this repo. Therefore, you should **always commit while the virtual environment is active**. * Your commit can be blocked if a check fails. This is normal and protects the repo. -* *Always check if your changes have been committed and pushed successfully!* +* **Always check if your changes have been committed and pushed successfully!** * You can also run the pre-commit checks manually on all files before committing, using: `pre-commit run --all-files` * If a pre commit check fails and you are stuck, see the troubleshooting section in diff --git a/USAGE.md b/docs/1_usage.md similarity index 100% rename from USAGE.md rename to docs/1_usage.md diff --git a/docs/1_secret_management.md b/docs/2_secret_management.md similarity index 100% rename from docs/1_secret_management.md rename to docs/2_secret_management.md diff --git a/docs/2_dependency_management.md b/docs/3_dependency_management.md similarity index 100% rename from docs/2_dependency_management.md rename to docs/3_dependency_management.md diff --git a/docs/3_pre_commit_hooks.md b/docs/5_pre_commit_hooks.md similarity index 100% rename from docs/3_pre_commit_hooks.md rename to docs/5_pre_commit_hooks.md From a04ead6d03c542d8ec18fdfc383fd86ade4a58a8 Mon Sep 17 00:00:00 2001 From: divaanster Date: Mon, 29 Sep 2025 11:39:43 +0200 Subject: [PATCH 069/199] replaces 0_setup.md --- SETUP.md | 127 ----------------------------------- docs/0_setup.md | 172 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 152 insertions(+), 147 deletions(-) delete mode 100644 SETUP.md diff --git a/SETUP.md b/SETUP.md deleted file mode 100644 index d511fe08..00000000 --- a/SETUP.md +++ /dev/null @@ -1,127 +0,0 @@ - -### ๐Ÿ“„ `SETUP.md` - Project Setup Guide - -This guide will walk you through setting up the `python_basic_template` development environment on your local machine. - -## ๐Ÿš€ Quick Start: One-Command Setup - -These scripts automate the entire setup process, including dependency installation, virtual environment creation, and tool configuration. - -### Prerequisites - -Before you begin, ensure you have the following installed: - -* **Python 3.12 or later** -* **Git** -* **1Password CLI**: Required for secret management. Follow the [official installation guide](https://developer.1password.com/docs/cli/get-started/). -* **cURL**: Usually pre-installed on Linux/macOS. -* **Sudo/Administrator access**: For installing system packages like `direnv`. - ---- - -### Linux / macOS Setup - -Open your terminal and run the following commands: - -```bash -# 1. Clone the repository -git clone https://github.com/thom-techlete/python_basic_template.git - -# 2. Navigate into the project directory -cd python_basic_template - -# 3. Run the setup script -./scripts/setup.sh``` - ---- - -### Windows Setup - -Open PowerShell and run the following commands: - -```powershell -# 1. Clone the repository -git clone https://github.com/thom-techlete/python_basic_template.git - -# 2. Navigate into the project directory -cd python_basic_template - -# 3. Run the setup script -# If activation is blocked: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned -.\scripts\setup.ps1 -``` - -After the script finishes, **restart your terminal or PowerShell session** to ensure all environment changes are applied correctly. - -## ๐Ÿค” What the Setup Script Does - -The setup script is designed to create a consistent and reproducible development environment by automating the following steps: - -1. **System Checks**: Verifies that Python, Git, and other required tools are installed. -2. **Virtual Environment**: Creates an isolated Python virtual environment in a `venv/` directory. -3. **Dependency Installation**: Installs all production and development dependencies using `pip-tools` for pinned, reproducible builds. -4. **Pre-Commit Hooks**: Sets up automated code quality checks that run before every commit. -5. **Secret Management**: Initializes `detect-secrets` to prevent accidental credential exposure and prepares for 1Password integration. -6. **Environment Variables**: Configures `direnv` (on Linux/macOS) to automatically manage environment variables when you enter the project directory. - -## ๐Ÿ›Ÿ Common Setup Issues - -
-Windows: PowerShell script execution is blocked - -If you see an error like `File .\setup.ps1 cannot be loaded... not digitally signed`, your system is blocking unsigned PowerShell scripts. - -**Solution:** -1. Open a new PowerShell window **as your normal user** (not as Administrator). -2. Run the following command to allow local scripts to run: - ```powershell - Set-ExecutionPolicy -Scope CurrentUser RemoteSigned - ``` -3. Close the PowerShell window and re-run the setup script in a new one. - -
- -
-Linux: `python3.12-venv` not found - -On some Linux distributions (like Debian/Ubuntu), the `venv` module is a separate package. The setup script will attempt to install it automatically. - -**Solution:** -If the automatic installation fails, you can install it manually: -```bash -sudo apt-get update -sudo apt-get install python3.12-venv -``` -Then, re-run `./scripts/setup.sh`. -
- -
-1Password CLI: Not signed in or not linked to WSL - -The setup script may warn that the `op` command-line tool is not signed in. - -**Solution:** -1. **Sign In**: Run `op account list`. If you see "No accounts configured," run `op signin` and follow the prompts. -2. **WSL Users**: If you use the 1Password desktop app on Windows, you must enable WSL integration in the app's settings: `Settings > Developer > Integrate with Windows Subsystem for Linux`. - -
- -
-Virtual Environment is Corrupted - -If you encounter strange dependency or pathing issues, your virtual environment might be in a bad state. - -**Solution:** -1. Delete the existing virtual environment directory. - ```bash - # On Linux/macOS - rm -rf venv - - # On Windows - Remove-Item -Recurse -Force venv - ``` -2. Re-run the setup script (`./scripts/setup.sh` or `.\scripts\setup.ps1`) to create a fresh one. -
- -*** - diff --git a/docs/0_setup.md b/docs/0_setup.md index e95df161..835dd80d 100644 --- a/docs/0_setup.md +++ b/docs/0_setup.md @@ -1,12 +1,146 @@ -# 0. Project setup -Setting up a new Python project requires several important steps to ensure a secure and efficient development workflowโ€”especially when working with secret management, virtual environments, and Git hygiene. +### ๐Ÿ“„ `0_setup.md` - Project Setup Guide + +This guide will walk you through setting up the `python_template` development environment on your local machine. + +## ๐Ÿš€ Quick Start: One-Command Setup + +These scripts automate the entire setup process, including dependency installation, virtual environment creation, and tool configuration. + +### Prerequisites + +Before you begin, ensure you have the following installed: + +* **Python 3.12 or later** +* **Git** +* **1Password CLI**: Required for secret management. Follow the [official installation guide](https://developer.1password.com/docs/cli/get-started/). +* **cURL**: Usually pre-installed on Linux/macOS. +* **Sudo/Administrator access**: For installing system packages like `direnv`. + +--- + +### Linux / macOS Setup + +Open your terminal and run the following commands: + +```bash +# 1. Clone the repository +git clone https://github.com/TECHLETES/{REPO_NAME}.git + +# 2. Navigate into the project directory +cd {REPO_NAME} + +# 3. Run the setup script +./scripts/setup.sh +``` --- -## **0.1 Prerequisite steps** +### Windows Setup + +Open PowerShell and run the following commands: + +```powershell +# 1. Clone the repository +git clone https://github.com/TECHLETES/{REPO_NAME}.git + +# 2. Navigate into the project directory +cd {REPO_NAME} + +# 3. Run the setup script +# If activation is blocked: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned +.\scripts\setup.ps1 +``` + +After the script finishes, **restart your terminal or PowerShell session** to ensure all environment changes are applied correctly. + +## ๐Ÿค” What the Setup Script Does + +The setup script is designed to create a consistent and reproducible development environment by automating the following steps: + +1. **System Checks**: Verifies that Python, Git, and other required tools are installed. +2. **Virtual Environment**: Creates an isolated Python virtual environment in a `venv/` directory. +3. **Dependency Installation**: Installs all production and development dependencies using `pip-tools` for pinned, reproducible builds. +4. **Pre-Commit Hooks**: Sets up automated code quality checks that run before every commit. +5. **Secret Management**: Initializes `detect-secrets` to prevent accidental credential exposure and prepares for 1Password integration. +6. **Environment Variables**: Configures `direnv` (on Linux/macOS) to automatically manage environment variables when you enter the project directory. + +## ๐Ÿ›Ÿ Common Setup Issues + +
+Windows: PowerShell script execution is blocked -### 0.1.1 Set-up 1Password (CLI) +If you see an error like `File .\setup.ps1 cannot be loaded... not digitally signed`, your system is blocking unsigned PowerShell scripts. + +**Solution:** +1. Open a new PowerShell window **as your normal user** (not as Administrator). +2. Run the following command to allow local scripts to run: + ```powershell + Set-ExecutionPolicy -Scope CurrentUser RemoteSigned + ``` +3. Close the PowerShell window and re-run the setup script in a new one. + +
+ +
+Linux: `python3.12-venv` not found + +On some Linux distributions (like Debian/Ubuntu), the `venv` module is a separate package. The setup script will attempt to install it automatically. + +**Solution:** +If the automatic installation fails, you can install it manually: +```bash +sudo apt-get update +sudo apt-get install python3.12-venv +``` +Then, re-run `./scripts/setup.sh`. +
+ +
+1Password CLI: Not signed in or not linked to WSL + +The setup script may warn that the `op` command-line tool is not signed in. + +**Solution:** +1. **Sign In**: Run `op account list`. If you see "No accounts configured," run `op signin` and follow the prompts. +2. **WSL Users**: If you use the 1Password desktop app on Windows, you must enable WSL integration in the app's settings: `Settings > Developer > Integrate with Windows Subsystem for Linux`. + +
+ +
+Virtual Environment is Corrupted + +If you encounter strange dependency or pathing issues, your virtual environment might be in a bad state. + +**Solution:** +1. Delete the existing virtual environment directory. + ```bash + # On Linux/macOS + rm -rf venv + + # On Windows + Remove-Item -Recurse -Force venv + ``` +2. Re-run the setup script (`./scripts/setup.sh` or `.\scripts\setup.ps1`) to create a fresh one. +
+ +*** + +## ๐Ÿงฐ Manual setup (for advanced users) + +If you prefer manual control, follow these steps in order. + +## **1 Prerequisites** + +Ensure the following tools are installed: + +- Python 3.12 or later +- Git +- curl +- 1Password CLI +- Sudo/admin access (for direnv, etc.) + +### 1.1 Set-up 1Password (CLI) 1. **Download and activate 1Password Desktop** 1. Download and install from here: @@ -49,7 +183,7 @@ Setting up a new Python project requires several important steps to ensure a sec --- -## 0.2 Automatic set-up script +## 2 Automatic set-up script To simplify the setup process, all mandatory setup steps have been implemented into a single, automated script. After cloning or forking this repository, you can configure your entire development environment by simply running: @@ -61,13 +195,13 @@ This script will take care of everything needed to get started, so you can focus If you'd prefer to walk through the setup manually or want to understand what's happening under the hood, a detailed breakdown of each step is provided in the sections below. -## 0.3 Manual set-up development environment +## 3 Manual set-up development environment Follow these steps to prepare your local machine for development with the Python Basic Template. Perform each section in order. --- -### 0.3.1 System Requirements Check +### 3.1 System Requirements Check 1. **Verify Python 3** @@ -99,7 +233,7 @@ If any of these commands fail, install the missing package before proceeding. --- -### 0.3.2 Virtual Environment Setup +### 3.2 Virtual Environment Setup 1. **Remove existing `venv`** (if present) @@ -128,7 +262,7 @@ If any of these commands fail, install the missing package before proceeding. --- -### 0.3.3 Hybrid Dependency Management +### 3.3 Hybrid Dependency Management This project uses a hybrid approach to dependency management: @@ -137,7 +271,7 @@ This project uses a hybrid approach to dependency management: You can use the helper script or perform each step manually: -**0.3.3.1 Usage of `dependency.sh`** +**3.3.1 Usage of `dependency.sh`** - Install both production and development dependencies: @@ -158,7 +292,7 @@ You can use the helper script or perform each step manually: ``` -**0.3.3.2 Manual Steps** +**3.3.2 Manual Steps** If you prefer to run each step manually, follow these commands: @@ -212,7 +346,7 @@ If you prefer to run each step manually, follow these commands: --- -### 0.3.4 System Locale Configuration +### 3.4 System Locale Configuration 1. **Update package lists and install locales** @@ -231,7 +365,7 @@ If you prefer to run each step manually, follow these commands: --- -### 0.3.5 direnv Setup +### 3.5 direnv Setup 1. **Install `direnv`** @@ -256,7 +390,7 @@ If you prefer to run each step manually, follow these commands: --- -### 0.3.6 Pre-commit Hooks Setup +### 3.6 Pre-commit Hooks Setup 1. **Install Git hooks** @@ -281,7 +415,7 @@ If you prefer to run each step manually, follow these commands: --- -### 0.3.7. Secret Management Setup +### 3.7. Secret Management Setup 1. **Check for 1Password CLI** @@ -306,7 +440,7 @@ If you prefer to run each step manually, follow these commands: --- -### 0.3.8 Development Tools Verification +### 3.8 Development Tools Verification 1. **Black** @@ -342,7 +476,7 @@ If you prefer to run each step manually, follow these commands: --- -### 0.3.9 Project Validation +### 3.9 Project Validation 1. **Ensure all modules import correctly** @@ -371,10 +505,8 @@ If you prefer to run each step manually, follow these commands: --- -### 0.3.10 Next Steps +### 3.10 Next Steps - Restart your terminal or run `source ~/.bashrc` - Start development: run your application or tests locally - Refer to the project README for usage and contribution guidelines - -***End of manual setup instructions.*** \ No newline at end of file From 377fe66005e25463508834c32c0eca281a4841d2 Mon Sep 17 00:00:00 2001 From: divaanster Date: Mon, 29 Sep 2025 11:51:05 +0200 Subject: [PATCH 070/199] updates readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 599b09d7..a09513b9 100644 --- a/README.md +++ b/README.md @@ -291,11 +291,11 @@ All development tools are configured in `pyproject.toml`: ## ๐Ÿ“š Documentation 0. **[Setup](docs/0_setup.md)** - Guide to setup the development environment -1. **[Secret Management](docs/1_secret_management.md)** - Secure secret handling guide -2. **[Dependency Management](docs/2_dependency_management.md)** - Detailed dependency workflow -3. **[Pre Commit Hooks](docs/3_pre_commit_hooks.md)** - Pre commit hooks to ensure safety and quality +1. **[Usage](docs/1_usage.md)** - Guide for daily usage of this repository +2. **[Secret Management](docs/2_secret_management.md)** - Secure secret handling guide +3. **[Dependency Management](docs/3_dependency_management.md)** - Detailed dependency workflow 4. **[Code Quality](docs/4_code_quality.md)** - Rules and guidelines on code quality and how it is enforced - +5. **[Pre Commit Hooks](docs/5_pre_commit_hooks.md)** - Pre commit hooks to ensure safety and quality ## ๐Ÿค Contributing From c1f19c1dd04a8b102ad0aff9971424eb75fe296c Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Sun, 12 Oct 2025 11:30:46 +0200 Subject: [PATCH 071/199] Remove faulty import & redundant final step --- scripts/setup.sh | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 3cf4b6ba..dcd7b994 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -253,22 +253,12 @@ setup_development_environment() { "$VENV_DIR/bin/python" - <<'PYCODE' import importlib -mods = ["utils.secrets", "utils.utils"] +mods = ["utils.secrets"] for m in mods: importlib.import_module(m) print("โœ… All project modules import successfully") PYCODE - find . -name "*.py" -not -path "./$VENV_DIR/*" -exec "$VENV_DIR/bin/python" -m py_compile {} \; - - if [ -s "$PROD_LOCK" ]; then - "$VENV_DIR/bin/python" -m pip check - success "dependencies compatible" - else - info "no production dependencies to verify" - fi - - section "setup complete" echo -e "\n${GREEN}๐ŸŽ‰ development environment setup complete${RESET}\n" echo -e "${BLUE}installed:${RESET}" From 854e287ebf9e02caa2873b8fd383b20b3aab827d Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Sun, 12 Oct 2025 11:39:24 +0200 Subject: [PATCH 072/199] Fix syntax error --- scripts/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index dcd7b994..c44f4154 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -61,7 +61,7 @@ setup_development_environment() { success "Python $(python3 --version | cut -d' ' -f2) found." # Ensure the venv module exists for the active python3 - PY_MINOR="$(python3 -c 'import sys; print(f\"{sys.version_info.major}.{sys.version_info.minor}\")')" + PY_MINOR="$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")" if python3 -c "import venv" &>/dev/null; then success "python${PY_MINOR} venv module is available." else From 0e39cd25fd583e6dcd935e3bd6418a4a074e9006 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 13 Oct 2025 16:31:36 +0200 Subject: [PATCH 073/199] Correct default naming of virtual environment --- scripts/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index c44f4154..54f5abbf 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -2,7 +2,7 @@ set -euo pipefail # configuration -export VENV_DIR="${VENV_DIR:-venv}" +export VENV_DIR="${VENV_DIR:-.venv}" export PYPROJECT="${PYPROJECT:-pyproject.toml}" export PROD_LOCK="${PROD_LOCK:-requirements.txt}" export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" From 2c60a6f18aca71d955c2850c232641098e711c0d Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 14 Oct 2025 09:36:54 +0200 Subject: [PATCH 074/199] Add linting pre-commit hooks --- .pre-commit-config.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f1d7f82e..a34f59e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,6 +33,21 @@ repos: language: python additional_dependencies: [black] types: [python] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-toml + - id: check-json + - id: check-symlinks + - id: check-yaml + - id: check-added-large-files + - id: detect-private-key + - id: pretty-format-json + args: ["--autofix"] + exclude: \.ipynb$ # Dependency vulnerability scanning - repo: https://github.com/pypa/pip-audit From db5ec64079b8ce60ea33ce13226c6f6128a6ade4 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 15 Oct 2025 14:17:56 +0200 Subject: [PATCH 075/199] Enhance pre-commit and mypy configuration - .pre-commit-config.yaml: - Add "-r requirements.txt" to pip-audit args to audit pinned deps. - Add commented frontend hook templates for optional web directory. - Minor formatting/quoting cleanups. - pyproject.toml: - Replace `strict = true` with explicit mypy strictness options (follow_imports, warn_*, no_implicit_optional, disallow_*, etc.). - Add [[tool.mypy.overrides]] for tests and scripts to relax rules. - Include "tests" in mypy exclude and remove duplicate overrides block. - Minor whitespace/comma cleanups. --- .pre-commit-config.yaml | 31 +++++++++++++++++++++++++++---- pyproject.toml | 38 ++++++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a34f59e2..8840e7da 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: language: python additional_dependencies: [black] types: [python] - + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 hooks: @@ -54,7 +54,7 @@ repos: rev: v2.9.0 hooks: - id: pip-audit - args: ['--progress-spinner', 'off'] + args: ["--progress-spinner", "off", "-r", "requirements.txt"] stages: [pre-commit] # Secrets scanning @@ -62,7 +62,7 @@ repos: rev: v1.5.0 hooks: - id: detect-secrets - args: ['--baseline', '.secrets.baseline'] + args: ["--baseline", ".secrets.baseline"] exclude: ^tests/ # Notebook/text sync @@ -88,7 +88,7 @@ repos: name: Ruff (linting + extra isort + rules) args: [--fix] types: [python] - + # - repo: local # hooks: # - id: pytest @@ -113,4 +113,27 @@ repos: always_run: true stages: [pre-commit] + # Frontend hooks (uncomment if you have a frontend in the `web/` directory) + # - id: frontend-lint + # name: Frontend lint + # entry: bash -c "cd web && npm run lint -- --fix" + # language: system + # files: ^web/ + # pass_filenames: false + # additional_dependencies: [] + + # - id: frontend-typecheck + # name: Frontend typecheck + # entry: bash -c "cd web && npm run typecheck" + # language: system + # files: ^web/ + # pass_filenames: false + # additional_dependencies: [] + # - id: frontend-tests + # name: Frontend tests + # entry: bash -c "cd web && npm run test:run" + # language: system + # files: ^web/ + # pass_filenames: false + # additional_dependencies: [] diff --git a/pyproject.toml b/pyproject.toml index c68a544d..5f95c349 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "pre-commit", - "detect-secrets", + "detect-secrets", "nbstripout", "pytest>=7.0", "pytest-cov", @@ -98,7 +98,7 @@ exclude_lines = [ "def __repr__", "if self.debug:", "if settings.DEBUG", - "raise AssertionError", + "raise AssertionError", "raise NotImplementedError", "if 0:", "if __name__ == .__main__.:", @@ -110,18 +110,40 @@ exclude_lines = [ [tool.mypy] python_version = "3.12" -strict = true + +# ignore_missing_imports for third-party libs without type hints ignore_missing_imports = true -show_error_codes = true +follow_imports = "silent" + +# strictness settings - adjust as needed warn_unused_ignores = true +warn_return_any = true +warn_unreachable = true +no_implicit_optional = true +allow_redefinition = true +show_error_codes = true show_column_numbers = true pretty = true +warn_unused_configs = true + +# enforce typing on own code +disallow_untyped_defs = true +disallow_incomplete_defs = true + exclude = [ - "migrations", # if Django/SQLAlchemy used - "notebooks", # if you sync with Jupytext + "migrations", + "notebooks", + "tests", ] plugins = [] +# Loosen rules for test and script files +[[tool.mypy.overrides]] +module = ["tests.*", "scripts.*"] +disallow_untyped_defs = false +disallow_incomplete_defs = false +warn_return_any = false + [tool.ruff] target-version = "py311" line-length = 88 @@ -145,10 +167,6 @@ lint.ignore = [ "E501", # Line too long (Black handles this) ] -[[tool.mypy.overrides]] -module = "tests.*" -disallow_untyped_defs = false - [tool.bandit] exclude_dirs = ["tests", "venv"] skips = ["B101", "B601"] From ea4cb4661f8c3deb53382c452f2d5f3608375e5e Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 15 Oct 2025 14:21:59 +0200 Subject: [PATCH 076/199] Run pip-audit against requirements.txt and remove leftover comment. Refactor/lint files to match new pre-commit hooks --- .detect-secrets.yaml | 2 +- .envrc | 1 - .github/workflows/ci.yml | 3 +- .gitignore | 4 +- README.md | 12 +- docs/0_setup.md | 188 ++++++++++++++-------------- docs/1_usage.md | 2 +- docs/2_secret_management.md | 36 +++--- docs/3_dependency_management.md | 24 ++-- docs/4_code_quality.md | 2 +- docs/5_pre_commit_hooks.md | 10 +- scripts/hooks/check-requirements.sh | 2 +- scripts/hooks/precommit_pytest.sh | 2 +- scripts/setup.ps1 | 1 - scripts/setup.sh | 4 +- 15 files changed, 145 insertions(+), 148 deletions(-) diff --git a/.detect-secrets.yaml b/.detect-secrets.yaml index 5a25d1cc..c73c9a1e 100644 --- a/.detect-secrets.yaml +++ b/.detect-secrets.yaml @@ -1,3 +1,3 @@ exclude: files: - - ^tests/ \ No newline at end of file + - ^tests/ diff --git a/.envrc b/.envrc index 1b795365..79a2a22b 100644 --- a/.envrc +++ b/.envrc @@ -15,4 +15,3 @@ export PYPROJECT="pyproject.toml" if [ -d "$VENV_DIR" ]; then source $VENV_DIR/bin/activate fi - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd84e065..4c9b0c9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: pip install --require-hashes -r requirements-dev.txt - name: Run pip-audit - run: pip-audit --progress-spinner off + run: pip-audit --progress-spinner off -r requirements.txt - name: Black formatting check run: black --check . @@ -80,4 +80,3 @@ jobs: with: commit_message: "chore: update coverage badge" file_pattern: coverage.svg - #test diff --git a/.gitignore b/.gitignore index 65ea155e..6e9760d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Don't use requirement.in or manually modified requirements.txt! +# Don't use requirement.in or manually modified requirements.txt! # We use pip-tools to manage dependencies. It reads and builds dpendencies from pyproject.toml. requirements.in @@ -99,4 +99,4 @@ __pypackages__/ notebooks-local/ # direnv -.direnv/ \ No newline at end of file +.direnv/ diff --git a/README.md b/README.md index a09513b9..ddaf160e 100644 --- a/README.md +++ b/README.md @@ -37,16 +37,16 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co return a + b Why: your editor can catch mistakes early and mypy can check types automatically * Put shared helpers in utils -* Use jupyter notebooks only for small tests / development. +* Use jupyter notebooks only for small tests / development. ### Git and commits * Pre commit is a tool that runs checks before every commit to keep code safe and consistent. -* **Important:** The pre-commit checks require certain packages that are installed in the virtual environment when you setup this repo. Therefore, you should **always commit while the virtual environment is active**. +* **Important:** The pre-commit checks require certain packages that are installed in the virtual environment when you setup this repo. Therefore, you should **always commit while the virtual environment is active**. * Your commit can be blocked if a check fails. This is normal and protects the repo. * **Always check if your changes have been committed and pushed successfully!** * You can also run the pre-commit checks manually on all files before committing, using: `pre-commit run --all-files` -* If a pre commit check fails and you are stuck, see the troubleshooting section in +* If a pre commit check fails and you are stuck, see the troubleshooting section in ## ๐Ÿš€ Quick Start @@ -168,11 +168,11 @@ This project uses a **hybrid dependency management approach**: ### Production dependencies -- Declared under `dependencies` in `pyproject.toml` +- Declared under `dependencies` in `pyproject.toml` - Locked to `requirements.txt` via pip-tools ### Development dependencies -- Declared under `[project.optional-dependencies].dev` in `pyproject.toml` +- Declared under `[project.optional-dependencies].dev` in `pyproject.toml` - Locked to `requirements-dev.txt` via pip-tools This setup ensures: @@ -290,7 +290,7 @@ All development tools are configured in `pyproject.toml`: ## ๐Ÿ“š Documentation -0. **[Setup](docs/0_setup.md)** - Guide to setup the development environment +0. **[Setup](docs/0_setup.md)** - Guide to setup the development environment 1. **[Usage](docs/1_usage.md)** - Guide for daily usage of this repository 2. **[Secret Management](docs/2_secret_management.md)** - Secure secret handling guide 3. **[Dependency Management](docs/3_dependency_management.md)** - Detailed dependency workflow diff --git a/docs/0_setup.md b/docs/0_setup.md index 835dd80d..ecae8d61 100644 --- a/docs/0_setup.md +++ b/docs/0_setup.md @@ -144,42 +144,42 @@ Ensure the following tools are installed: 1. **Download and activate 1Password Desktop** 1. Download and install from here: - + [Download 1Password for Windows | 1Password](https://1password.com/downloads/windows) - + 2. Log in with your account. 3. Go to โ€˜Settingsโ€™ > โ€˜Securityโ€™ > Enable โ€˜Unlock with Windows Helloโ€™ 4. Go to โ€˜settingsโ€™ > โ€˜Developerโ€™ > Enable โ€˜Integrate with 1Password CLIโ€™. - + 2. **Set-up 1password cli** 1. Open windows powershell and run - + ```bash winget install 1password-cli ``` - - 2. In your file explorer navigate to: - - `C:/Users//AppData/Local/Microsoft/WinGet/Packages/<1password_cli_package_id>/op.exe` and copy the absolute path that leads to the `op.exe` - + + 2. In your file explorer navigate to: + + `C:/Users//AppData/Local/Microsoft/WinGet/Packages/<1password_cli_package_id>/op.exe` and copy the absolute path that leads to the `op.exe` + 3. You then modify the path by replacing `C:`/ with `/mnt/c/` . Also replace all backward slashes with forward slashes. Your final path should look something like: - + `/mnt/c/Users/thome/AppData/Local/Microsoft/WinGet/Packages/AgileBits.1Password.CLI_Microsoft.Winget.Source_8wekyb3d8bbwe/op.exe` - + 4. Then in WSL run the following command: - + ```bash ln -s /usr/bin/op ``` - + 5. Run in WSL: - + ```bash op --version ``` - + If you see a version number, then the link was succesfull and you can access 1Password in WSL - + --- @@ -204,30 +204,30 @@ Follow these steps to prepare your local machine for development with the Python ### 3.1 System Requirements Check 1. **Verify Python 3** - + ```bash python3 --version ``` - + - Ensure the version is **3.12** or later. 2. **Verify pip** - + ```bash pip --version ``` - + 3. **Verify curl** - + ```bash command -v curl ``` - + 4. **Verify git** - + ```bash git --version ``` - + If any of these commands fail, install the missing package before proceeding. @@ -236,29 +236,29 @@ If any of these commands fail, install the missing package before proceeding. ### 3.2 Virtual Environment Setup 1. **Remove existing `venv`** (if present) - + ```bash rm -rf venv ``` - + 2. **Create a new virtual environment** - + ```bash python3 -m venv venv ``` - + 3. **Activate the virtual environment** - + ```bash source venv/bin/activate ``` - + 4. **Upgrade pip** - + ```bash pip install --upgrade pip ``` - + --- @@ -274,234 +274,234 @@ You can use the helper script or perform each step manually: **3.3.1 Usage of `dependency.sh`** - Install both production and development dependencies: - + ``` ./scripts/dependency.sh ``` - + - Install only production dependencies: - + ``` ./scripts/dependency.sh --prod ``` - + - Install only development dependencies: - + ``` ./scripts/dependency.sh --dev ``` - + **3.3.2 Manual Steps** If you prefer to run each step manually, follow these commands: 1. **Activate the virtual environment** (must be in project root): - + ``` source venv/bin/activate ``` - + 2. **Ensure `pip-tools` is installed**: - + ``` pip show pip-tools &>/dev/null || pip install pip-tools ``` - + 3. **Compile production lockfile**: - + ``` pip-compile pyproject.toml \ --output-file=requirements.txt \ --generate-hashes ``` - + 4. **Install production dependencies**: - + ``` pip install --require-hashes -r requirements.txt ``` - + 5. **Compile development lockfile**: - + ``` pip-compile pyproject.toml \ --extra=dev \ --output-file=requirements-dev.txt \ --generate-hashes ``` - + 6. **Install development dependencies**: - + ``` pip install --require-hashes -r requirements-dev.txt ``` - + 7. **Verify that all packages are compatible**: - + ``` pip check ``` - + --- ### 3.4 System Locale Configuration 1. **Update package lists and install locales** - + ```bash sudo apt update sudo apt install -y locales ``` - + 2. **Generate and apply the `en_US.UTF-8` locale** - + ```bash sudo locale-gen en_US.UTF-8 sudo update-locale LANG=en_US.UTF-8 ``` - + --- ### 3.5 direnv Setup 1. **Install `direnv`** - + ```bash sudo apt install -y direnv ``` - + 2. **Add the `direnv` hook to your shell profile** - + ```bash echo 'eval "$(direnv hook bash)"' >> ~/.bashrc source ~/.bashrc ``` - + 3. **Allow the project `.envrc`** - + ```bash cd path/to/project direnv allow ``` - + --- ### 3.6 Pre-commit Hooks Setup 1. **Install Git hooks** - + ```bash pre-commit install ``` - + 2. **Update hook definitions** - + ```bash pre-commit autoupdate - + ``` - + 3. **Run all checks once** - + ```bash pre-commit run --all-files - + ``` - + --- ### 3.7. Secret Management Setup 1. **Check for 1Password CLI** - + ```bash op --version ``` - + 2. **Verify your 1Password account** - + ```bash op account list ``` - + 3. **Initialize a baseline for `detect-secrets`** - + ```bash mv .secrets.baseline .secrets.baseline.bak 2>/dev/null || true detect-secrets scan > .secrets.baseline detect-secrets audit .secrets.baseline ``` - + --- ### 3.8 Development Tools Verification 1. **Black** - + ```bash pip show black ``` - + 2. **Ruff** - + ```bash pip show ruff ``` - + 3. **Mypy** - + ```bash pip show mypy ``` - + 4. **Beartype** - + ```bash pip show beartype ``` - + 5. **pytest** - + ```bash pip show pytest - + ``` - + --- ### 3.9 Project Validation 1. **Ensure all modules import correctly** - + ```bash python3 - << 'EOF' import utils.secrets import utils.utils print("Imports successful") EOF - + ``` - + 2. **Syntax check all Python files** - + ```bash find . -name "*.py" -not -path "./venv/*" \ -exec python3 -m py_compile {} + ``` - + 3. **Verify installed dependencies** - + ```bash pip check ``` - + --- diff --git a/docs/1_usage.md b/docs/1_usage.md index f9228d61..e25eb829 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -142,4 +142,4 @@ This project uses a hybrid approach where dependencies are declared in `pyprojec * **To Add a Development-Only Dependency**: 1. Add the package name (e.g., `"ipykernel"`) to the `dev` list under `[project.optional-dependencies]` in `pyproject.toml`. - 2. Run the dependency script: `./scripts/dependency.sh` \ No newline at end of file + 2. Run the dependency script: `./scripts/dependency.sh` diff --git a/docs/2_secret_management.md b/docs/2_secret_management.md index 810c8f64..853ff5f4 100644 --- a/docs/2_secret_management.md +++ b/docs/2_secret_management.md @@ -5,52 +5,52 @@ We use **1Password CLI** (`op`) and **GitHub Secrets** to fetch secrets securely ## 1.1 Local Development 1. **Authentication for 1Password is handled automatically** - + Once per session you will be asked to authenticate through the 1Password Interface. **Never** log in manually through the cli. This will overwrite the connection. - + 2. **Enable Direnv when changing .envrc** Whenever you make any changes to `.envrc` always allow Direnv to load environment variables automatically: - + ```bash direnv allow ``` - + 3. **Environment Variables** We load secrets on demand from 1Passwordโ€”no `.env` file. This can be done dynamically using the provided utility function `get_secret` in `utils/secrets.py` . Use it as follows: - + ```python import os from utils.secrets import get_secret - + # To find the path to your secret go to 1Password Interace > your item > click dropdown next to field > "Copy secret reference" - + # Load once using API_KEY = get_secret("op://Shared with all/RAGFLow API Key/credential") - + # Or load and save to use accross the environment: get_secret("op://Shared with all/RAGFLow API Key/credential", "API_KEY") API_KEY = os.getenv('API_KEY') ``` - + - + 4. **Pre-commit Hooks** On each commit, secrets are blocked and notebook outputs cleared: - `detect-secrets` - `nbstripout` - + These hooks are configured in `.pre-commit-config.yaml` and installed via: - + ```bash pre-commit install pre-commit autoupdate ``` - + --- @@ -97,8 +97,8 @@ This way, CI runners receive secrets securely from GitHub and never see 1Passwor ## 1.3 Rotating & Auditing - **Rotate** immediately in 1Password or GitHub when a secret is compromised. Follow this action plan: - + [Compromised secret action plan](https://www.notion.so/Compromised-secret-action-plan-24517d03144d805eba09e65cd5fe6a49?pvs=21) - + - **Audit** with `detect-secrets scan > .secrets.baseline` after updates. -- Maintain a clear **playbook** in `SECURITY.md` for incident response. \ No newline at end of file +- Maintain a clear **playbook** in `SECURITY.md` for incident response. diff --git a/docs/3_dependency_management.md b/docs/3_dependency_management.md index 54df0078..97887350 100644 --- a/docs/3_dependency_management.md +++ b/docs/3_dependency_management.md @@ -37,29 +37,29 @@ The provided script automates compilation and installation of both production an 1. Activates the project virtual environment (`venv/bin/activate`). 2. Installs or verifies **pip-tools**. 3. Compiles the production lockfile from `pyproject.toml`: - + ```bash pip-compile pyproject.toml --output-file=requirements.txt --generate-hashes ``` - + 4. Installs production packages: - + ```bash pip install --require-hashes -r requirements.txt ``` - + 5. Compiles the development lockfile from the `dev` extras: - + ```bash pip-compile pyproject.toml --extra=dev --output-file=requirements-dev.txt --generate-hashes ``` - + 6. Installs development packages: - + ```bash pip install --require-hashes -r requirements-dev.txt ``` - + 7. Runs `pip check` to validate compatibility. --- @@ -85,14 +85,14 @@ If migrating from a legacy `requirements.in` workflow: 2. **Move all tooling dependencies** into `[project.optional-dependencies.dev]`. 3. **Remove** the old `requirements.in` file. 4. **Regenerate** lockfiles: - + ```bash pip-compile pyproject.toml --output-file=requirements.txt --generate-hashes pip-compile pyproject.toml --extra=dev --output-file=requirements-dev.txt --generate-hashes ``` - + 5. **Install** with: - + ```bash ./scripts/dependency.sh - ``` \ No newline at end of file + ``` diff --git a/docs/4_code_quality.md b/docs/4_code_quality.md index 47ca7399..ee04bded 100644 --- a/docs/4_code_quality.md +++ b/docs/4_code_quality.md @@ -138,4 +138,4 @@ Or use pre-commit to run all configured hooks: ```bash pre-commit run --all-files -``` \ No newline at end of file +``` diff --git a/docs/5_pre_commit_hooks.md b/docs/5_pre_commit_hooks.md index 0a00d06c..7195a11d 100644 --- a/docs/5_pre_commit_hooks.md +++ b/docs/5_pre_commit_hooks.md @@ -135,19 +135,19 @@ This is useful to verify changes or fix all issues in one go. ### 3.5.1 Locale warnings (WSL or CI environments) - If you see: - + ```bash setlocale: LC_ALL: cannot change locale (en_US.UTF-8) ``` - + - Fix it by running: - + ```bash sudo apt install locales sudo locale-gen en_US.UTF-8 sudo update-locale LANG=en_US.UTF-8 ``` - + ### 3.5.2 `nbstripout: command not found` @@ -183,4 +183,4 @@ If you have formatting conflicts: - Let Black handle all formatting automatically - Avoid manual formatting that conflicts with Black's style -- Use `# fmt: off` and `# fmt: on` comments to disable Black for specific sections if absolutely necessary \ No newline at end of file +- Use `# fmt: off` and `# fmt: on` comments to disable Black for specific sections if absolutely necessary diff --git a/scripts/hooks/check-requirements.sh b/scripts/hooks/check-requirements.sh index da693608..32cc1b27 100755 --- a/scripts/hooks/check-requirements.sh +++ b/scripts/hooks/check-requirements.sh @@ -13,4 +13,4 @@ if git diff --cached --name-only | grep -q "^requirements-dev.txt$"; then echo "Use 'pip-compile' instead. See 2.2 in the docs." exit 1 fi -fi \ No newline at end of file +fi diff --git a/scripts/hooks/precommit_pytest.sh b/scripts/hooks/precommit_pytest.sh index be66054f..215099e9 100755 --- a/scripts/hooks/precommit_pytest.sh +++ b/scripts/hooks/precommit_pytest.sh @@ -15,4 +15,4 @@ fi pip install -q -e .[dev] # Run pytest with coverage and beartype runtime checks -pytest --cov --cov-fail-under=80 --beartype-packages=utils,example \ No newline at end of file +pytest --cov --cov-fail-under=80 --beartype-packages=utils,example diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index 9311ed3a..10086a0a 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -73,4 +73,3 @@ $env:PYTHONPATH = (Get-Location).Path python -c "import utils.secrets; print('โœ… All project modules import successfully')" Ok "๐ŸŽ‰ Windows setup complete" - diff --git a/scripts/setup.sh b/scripts/setup.sh index 54f5abbf..d363ced1 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -97,7 +97,7 @@ setup_development_environment() { warn "direnv not found." install_package "direnv" fi - + if has_cmd curl; then success "curl available."; else warn "curl not found. Some optional steps may be skipped."; fi @@ -226,7 +226,7 @@ setup_development_environment() { if ! command -v detect-secrets >/dev/null 2>&1; then python -m pip install detect-secrets fi - + step "initializing secrets baseline" if [ -f ".secrets.baseline" ]; then warn "existing .secrets.baseline found. backing up" From 48b6efc0eeb55a7b4d60be8688a9ed4e3468b7e0 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 15 Oct 2025 14:29:19 +0200 Subject: [PATCH 077/199] Pin pip-tools to 7.5.0 for lockfile verification; remove legacy check-requirements hooks; normalize .vscode/settings.json formatting --- .pre-commit-config.yaml | 4 ++-- .vscode/settings.json | 16 ++++++++++------ scripts/hooks/check-requirements.ps1 | 11 ----------- scripts/hooks/check-requirements.sh | 16 ---------------- 4 files changed, 12 insertions(+), 35 deletions(-) delete mode 100644 scripts/hooks/check-requirements.ps1 delete mode 100755 scripts/hooks/check-requirements.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8840e7da..82e82549 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: args: ["requirements.txt"] language: python files: ^requirements\.txt$ - additional_dependencies: [pip-tools] + additional_dependencies: [pip-tools==7.5.0] - id: verify-dev-lockfile name: Verify development lockfile is up-to-date @@ -16,7 +16,7 @@ repos: args: ["requirements-dev.txt"] language: python files: ^requirements-dev\.txt$ - additional_dependencies: [pip-tools] + additional_dependencies: [pip-tools==7.5.0] # Strip notebook outputs - id: nbstripout-autoadd diff --git a/.vscode/settings.json b/.vscode/settings.json index a8737fc5..e6a20a5b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,12 @@ { - "python.analysis.typeCheckingMode": "standard", - "python.linting.mypyEnabled": true, - "python.linting.enabled": true, - "python.linting.mypyArgs": ["--config-file=pyproject.toml"], - "ruff.enable": true, - "ruff.args": ["--config=pyproject.toml"], + "python.analysis.typeCheckingMode": "standard", + "python.linting.enabled": true, + "python.linting.mypyArgs": [ + "--config-file=pyproject.toml" + ], + "python.linting.mypyEnabled": true, + "ruff.args": [ + "--config=pyproject.toml" + ], + "ruff.enable": true } diff --git a/scripts/hooks/check-requirements.ps1 b/scripts/hooks/check-requirements.ps1 deleted file mode 100644 index dbaafa66..00000000 --- a/scripts/hooks/check-requirements.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -param([string[]]$Args) - -$changed = git diff --cached --name-only -$targets = @('requirements.txt', 'requirements-dev.txt') -$edited = $changed | Where-Object { $targets -contains $_ } - -if ($edited) { - Write-Host "requirements files changed. Use pip-compile to update lock files." -ForegroundColor Red - exit 1 -} -exit 0 diff --git a/scripts/hooks/check-requirements.sh b/scripts/hooks/check-requirements.sh deleted file mode 100755 index 32cc1b27..00000000 --- a/scripts/hooks/check-requirements.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -if git diff --cached --name-only | grep -q "^requirements.txt$"; then - if ! grep -q "# This file is autogenerated by pip-compile" requirements.txt; then - echo "โŒ ERROR: requirements.txt appears to have been modified manually." - echo "Use 'pip-compile' instead. See 2.2 in the docs." - exit 1 - fi -fi - -if git diff --cached --name-only | grep -q "^requirements-dev.txt$"; then - if ! grep -q "# This file is autogenerated by pip-compile" requirements-dev.txt; then - echo "โŒ ERROR: requirements-dev.txt appears to have been modified manually." - echo "Use 'pip-compile' instead. See 2.2 in the docs." - exit 1 - fi -fi From 4a6c5169594a2eec1e5ea72be8eb4ebe4ab56f1c Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 15 Oct 2025 14:33:34 +0200 Subject: [PATCH 078/199] Regenerate requirements.txt: bump certifi, charset-normalizer, idna, and markupsafe with updated hashes --- requirements.txt | 358 +++++++++++++++++++++++++++-------------------- 1 file changed, 210 insertions(+), 148 deletions(-) diff --git a/requirements.txt b/requirements.txt index bc7ab5db..927b0d33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,90 +8,124 @@ blinker==1.9.0 \ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc # via flask -certifi==2025.8.3 \ - --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ - --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 +certifi==2025.10.5 \ + --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ + --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 # via requests -charset-normalizer==3.4.3 \ - --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ - --hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \ - --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ - --hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \ - --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ - --hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \ - --hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \ - --hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \ - --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ - --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ - --hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \ - --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ - --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ - --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ - --hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \ - --hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \ - --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ - --hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \ - --hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \ - --hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \ - --hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \ - --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ - --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ - --hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \ - --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ - --hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \ - --hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \ - --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ - --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ - --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ - --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ - --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ - --hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \ - --hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \ - --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ - --hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \ - --hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \ - --hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \ - --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ - --hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \ - --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ - --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ - --hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \ - --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ - --hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \ - --hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \ - --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ - --hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \ - --hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \ - --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ - --hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \ - --hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \ - --hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \ - --hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \ - --hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \ - --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ - --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ - --hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \ - --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ - --hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \ - --hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \ - --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ - --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ - --hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \ - --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ - --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ - --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ - --hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \ - --hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \ - --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ - --hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \ - --hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \ - --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ - --hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \ - --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ - --hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \ - --hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \ - --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ - --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 +charset-normalizer==3.4.4 \ + --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ + --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ + --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ + --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ + --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ + --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ + --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ + --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ + --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ + --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ + --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ + --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ + --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ + --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ + --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ + --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ + --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ + --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ + --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ + --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ + --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ + --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ + --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ + --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ + --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ + --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ + --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ + --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ + --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ + --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ + --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ + --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ + --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ + --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ + --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ + --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ + --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ + --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ + --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ + --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ + --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ + --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ + --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ + --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ + --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ + --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ + --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ + --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ + --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ + --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ + --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ + --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ + --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ + --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ + --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ + --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ + --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ + --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ + --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ + --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ + --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ + --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ + --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ + --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ + --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ + --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ + --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ + --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ + --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ + --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ + --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ + --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ + --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ + --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ + --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ + --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ + --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ + --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ + --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ + --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ + --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ + --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ + --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ + --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ + --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ + --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ + --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ + --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ + --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ + --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ + --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ + --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ + --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ + --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ + --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ + --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ + --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ + --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ + --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ + --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ + --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ + --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ + --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ + --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ + --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ + --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ + --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ + --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ + --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ + --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ + --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ + --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ + --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 # via requests click==8.3.0 \ --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ @@ -101,9 +135,9 @@ flask==2.3.3 \ --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b # via python-basic-template (pyproject.toml) -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 # via requests itsdangerous==2.2.0 \ --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ @@ -113,68 +147,96 @@ jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via flask -markupsafe==3.0.2 \ - --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \ - --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ - --hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \ - --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ - --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ - --hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \ - --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ - --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \ - --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ - --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \ - --hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \ - --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \ - --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \ - --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ - --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ - --hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \ - --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ - --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ - --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ - --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \ - --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \ - --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ - --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \ - --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \ - --hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \ - --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \ - --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \ - --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ - --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ - --hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \ - --hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \ - --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ - --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \ - --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \ - --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ - --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \ - --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \ - --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ - --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ - --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \ - --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ - --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ - --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ - --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \ - --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ - --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ - --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ - --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ - --hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \ - --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ - --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \ - --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \ - --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ - --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ - --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ - --hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \ - --hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \ - --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ - --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ - --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \ - --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50 +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via # jinja2 # werkzeug From 5b0a152f6deb5846df3093bfc4e6b7db4d930107 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 15 Oct 2025 14:39:20 +0200 Subject: [PATCH 079/199] Add GitHub issue templates and contact config Add ISSUE_TEMPLATE files for bug reports, documentation updates, and feature requests to standardize and streamline issue submissions. Also add config.yml with contact links and blank_issues_enabled: false. --- .../workflows/ISSUE_TEMPLATE/bug-report.md | 38 ++++++++++ .github/workflows/ISSUE_TEMPLATE/config.yml | 14 ++++ .../workflows/ISSUE_TEMPLATE/docs_update.md | 72 +++++++++++++++++++ .../ISSUE_TEMPLATE/feature-request.md | 61 ++++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 .github/workflows/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/workflows/ISSUE_TEMPLATE/config.yml create mode 100644 .github/workflows/ISSUE_TEMPLATE/docs_update.md create mode 100644 .github/workflows/ISSUE_TEMPLATE/feature-request.md diff --git a/.github/workflows/ISSUE_TEMPLATE/bug-report.md b/.github/workflows/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000..da3e5ada --- /dev/null +++ b/.github/workflows/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,38 @@ +--- +name: "๐Ÿ› Bug report" +about: Report an issue or unexpected behavior +title: "[Bug] " +labels: bug +assignees: "" +--- + +## Description +A clear and concise description of the bug. + +## Steps to reproduce +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +## Expected behavior +What did you expect to happen? + +## Actual behavior +What actually happened? + +## Screenshots / logs +If applicable, add screenshots or log snippets to help explain the problem. + +## Environment +- Device: [e.g. iPhone 13, Desktop] +- OS: [e.g. iOS 17, Windows 11] +- Browser/app version: [e.g. Chrome 139, v1.2.3] + +## Additional context +Add any other context about the problem here. + +## Acceptance criteria +- [ ] Bug can be reproduced consistently. +- [ ] Bug is fixed and expected behavior is verified. +- [ ] No regression introduced in related features. diff --git a/.github/workflows/ISSUE_TEMPLATE/config.yml b/.github/workflows/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..f9d248fc --- /dev/null +++ b/.github/workflows/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: ๐Ÿ’ฌ General Discussion + url: https://github.com/TECHLETES/python_template/discussions + about: For questions, ideas, and general discussion about the python_template project + - name: ๐Ÿ”’ Security Issue + url: https://github.com/TECHLETES/python_template/security/advisories/new + about: Report security vulnerabilities privately + - name: ๐Ÿ“– Documentation + url: https://github.com/TECHLETES/python_template/blob/main/README.md + about: Read the project documentation and setup guides + - name: ๐Ÿš€ Support + url: https://github.com/TECHLETES/python_template/discussions/categories/q-a + about: Get help and support from the community diff --git a/.github/workflows/ISSUE_TEMPLATE/docs_update.md b/.github/workflows/ISSUE_TEMPLATE/docs_update.md new file mode 100644 index 00000000..ce07b8d3 --- /dev/null +++ b/.github/workflows/ISSUE_TEMPLATE/docs_update.md @@ -0,0 +1,72 @@ +--- +name: Documentation Update +about: Suggest improvements to documentation +title: '[DOCS] ' +labels: ['documentation'] +assignees: '' +--- + +## Documentation Location +**Which documentation needs to be updated?** +- [ ] README.md +- [ ] API Documentation +- [ ] User Guide/Tutorial +- [ ] Developer Setup Guide +- [ ] Code Comments/Docstrings +- [ ] GitHub Wiki +- [ ] Other: _______________ + +**Specific file/section/page:** +Please provide the specific file path, page URL, or section name that needs updating. + +## Issue Description +**What's wrong or missing?** +Clearly describe the documentation issue: +- [ ] Information is outdated +- [ ] Information is incorrect +- [ ] Information is missing +- [ ] Information is confusing/unclear +- [ ] Broken links or references +- [ ] Formatting issues +- [ ] Other: _______________ + +## Current Content +**What does the documentation currently say?** +Quote or describe the current content that needs to be changed (if applicable). + +## Suggested Changes +**What should it say instead?** +Provide the corrected, updated, or additional content. Be as specific as possible. + +## Context/Background +**Why is this change needed?** +Help us understand: +- What confusion or problems does the current documentation cause? +- What recent changes in the codebase made this documentation outdated? +- What use case or scenario is not covered? + +## Additional Information +**Target audience for this documentation:** +- [ ] End users +- [ ] Developers/Contributors +- [ ] System administrators +- [ ] All users + +**Priority level:** +- [ ] Low - Minor improvement +- [ ] Medium - Important for user experience +- [ ] High - Critical information that's missing/wrong + +## Related Issues +**Link any related issues or PRs** +If this documentation update is related to a bug fix, feature addition, or other issue, please link it here. + +## Proposed Format +**If suggesting new content, what format works best?** +- [ ] Step-by-step tutorial +- [ ] Reference documentation +- [ ] FAQ entry +- [ ] Code examples +- [ ] Screenshots/diagrams +- [ ] Video walkthrough +- [ ] Other: _______________ diff --git a/.github/workflows/ISSUE_TEMPLATE/feature-request.md b/.github/workflows/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 00000000..13f18876 --- /dev/null +++ b/.github/workflows/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,61 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[FEATURE] ' +labels: ['enhancement'] +assignees: '' +--- + +## Feature Summary +**Describe the feature in one sentence** +A clear and concise summary of the feature you'd like to see implemented. + +## Problem Statement +**What problem does this solve?** +Describe the problem or limitation you're facing that this feature would address. Help us understand the "why" behind the request. + +## Proposed Solution +**Describe your ideal solution** +A clear and detailed description of what you want to happen. Include: +- How should it work? +- What should the user experience be like? +- Any specific requirements or constraints? + +## Alternative Solutions +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. This could include: +- Different approaches to solving the same problem +- Existing tools or workarounds you've tried +- Similar features in other platforms + +## Use Case / User Story +**Who would benefit and how?** +Describe the user story and use case: +- As a [type of user], I want [goal] so that [benefit] +- How many users would benefit from this? +- What types of workflows would this improve? + +## Implementation Ideas +**If you have ideas about implementation, share them** +(Optional) Any thoughts on how this could be implemented: +- Which part of the system should this be added to? +- Any technical considerations or dependencies? +- Database changes needed? +- UI/UX considerations? + +## Additional Context +**Any other context, mockups, or examples** +Add any other context, screenshots, mockups, links to similar features, or examples that would help explain your request. + +## Priority/Impact +**How important is this feature?** +- [ ] Low - Nice to have +- [ ] Medium - Would improve workflow significantly +- [ ] High - Critical for our use case +- [ ] Blocking - We can't proceed without this + +**Estimated impact:** +- [ ] Affects few users +- [ ] Affects many users +- [ ] Affects all users +- [ ] Affects core functionality From f33150970b7b1a58dde04b06ea24e81928dd52d4 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 15 Oct 2025 15:05:16 +0200 Subject: [PATCH 080/199] Refactor setup script, add Docker install, and update pre-commit deps - Split monolithic setup into smaller functions (check_system_requirements, setup_virtual_environment, setup_dependencies, configure_locale, setup_direnv, setup_pre_commit, setup_secrets, verify_tools, validate_project, print_completion) and orchestrate them from setup_development_environment. - Add robust Docker & Docker Compose installation flow for Debian/Ubuntu (add GPG key, set up repo, install packages, enable/start service). - Improve get_ssh_key parsing to correctly detect Host blocks and resolve identity file to an absolute path with a sensible fallback. - Enhance pre-commit setup: install hooks, autoupdate repos, and run hooks against all files. - Ensure detect-secrets is available before scanning and refresh secrets baseline. - Bump pre-commit integrations: pre-commit-hooks -> v6.0.0, jupytext -> v1.18.0rc0, ruff -> v0.14.0. - Refresh .secrets.baseline generated_at timestamp. --- .pre-commit-config.yaml | 6 +- .secrets.baseline | 2 +- scripts/setup.sh | 122 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 117 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82e82549..6fc88cf7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: types: [python] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -67,7 +67,7 @@ repos: # Notebook/text sync - repo: https://github.com/mwouts/jupytext - rev: v1.17.3 + rev: v1.18.0rc0 hooks: - id: jupytext args: [--sync] @@ -82,7 +82,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.1 + rev: v0.14.0 hooks: - id: ruff name: Ruff (linting + extra isort + rules) diff --git a/.secrets.baseline b/.secrets.baseline index d05b2c0c..ee168433 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -146,5 +146,5 @@ } ] }, - "generated_at": "2025-08-13T14:21:51Z" + "generated_at": "2025-10-15T13:02:47Z" } diff --git a/scripts/setup.sh b/scripts/setup.sh index d363ced1..58d72c7d 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -29,6 +29,31 @@ is_wsl() { grep -qi "microsoft" /proc/sys/kernel/osrelease 2>/dev/null || [[ -n "${WSL_DISTRO_NAME:-}" ]] } +get_ssh_key() { + local host="github.com" + local config_file="$HOME/.ssh/config" + local key="" + + if [ -f "$config_file" ]; then + key=$(awk -v host="$host" ' + tolower($0) ~ tolower("host " host) { in_block=1; next } + in_block && /^host / { in_block=0 } + in_block && tolower($1) == "identityfile" { print $2; exit } + ' "$config_file") + fi + + # If key found, make it absolute path + if [ -n "$key" ]; then + if [[ $key != /* ]]; then + key="$HOME/.ssh/$key" + fi + else + # Fallback to default + key="$HOME/.ssh/id_rsa" + fi + + echo "$key" +} has_cmd() { command -v "$1" >/dev/null 2>&1; } # Universal package installation function @@ -54,7 +79,7 @@ install_package() { } -setup_development_environment() { +check_system_requirements() { section "system requirements check" step "verifying required system packages" has_cmd python3 || error "Python 3 is not installed. Install Python 3.12+." @@ -79,7 +104,6 @@ setup_development_environment() { fi fi - # Check for pip and install if missing if has_cmd pip; then success "pip $(pip --version | cut -d' ' -f2) found." @@ -100,7 +124,63 @@ setup_development_environment() { if has_cmd curl; then success "curl available."; else warn "curl not found. Some optional steps may be skipped."; fi + section "docker setup" + step "checking for Docker and Docker Compose" + if has_cmd apt-get; then + if ! command -v docker &> /dev/null; then + step "Docker not found. Installing Docker..." + + # Remove old versions + sudo apt-get remove -y docker docker-engine docker.io containerd runc || true + + # Update packages + sudo apt-get update -y + sudo apt-get install -y ca-certificates curl gnupg lsb-release + + # Add Dockerโ€™s official GPG key + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg + + # Set up the Docker repository + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + + # Install Docker + sudo apt-get update -y + sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + # Enable & start Docker + sudo systemctl enable docker + sudo systemctl start docker + + success "Docker installed successfully." + else + success "Docker is already installed." + fi + + # Check & install Docker Compose (v2 CLI plugin) + if ! docker compose version &> /dev/null; then + step "Docker Compose plugin not found. Installing..." + sudo apt-get install -y docker-compose-plugin + success "Docker Compose installed successfully." + else + success "Docker Compose is already installed." + fi + + # Verification + step "verifying installation" + docker --version + docker compose version + success "Docker and Docker Compose are ready to use!" + else + warn "Docker installation only supported on Debian/Ubuntu systems with apt-get." + fi +} + +setup_virtual_environment() { section "virtual environment setup" step "creating Python virtual environment" if [ -d "$VENV_DIR" ]; then @@ -135,12 +215,13 @@ setup_development_environment() { success "virtual environment created and activated" fi - step "upgrading pip in virtual environment" python -m ensurepip --upgrade >/dev/null 2>&1 || true python -m pip install --upgrade pip success "pip upgraded to $("$VENV_DIR/bin/python" -m pip --version | awk '{print $2}')." +} +setup_dependencies() { section "hybrid dependency management" info "production deps in $PYPROJECT to $PROD_LOCK" info "development deps in $PYPROJECT extra [dev] to $DEV_LOCK" @@ -151,7 +232,9 @@ setup_development_environment() { step "running dependency setup script" ./scripts/dependency.sh success "dependencies installed" +} +configure_locale() { section "system locale configuration" step "ensuring locales" if has_cmd apt-get; then @@ -170,8 +253,9 @@ setup_development_environment() { else warn "no known package manager detected for locale configuration" fi +} - +setup_direnv() { section "direnv setup" step "configuring direnv shell integration" PROFILE="${HOME}/.bashrc" @@ -188,7 +272,9 @@ setup_development_environment() { else warn ".envrc not found. skipping allow" fi +} +setup_pre_commit() { section "pre commit hooks setup" step "installing pre commit hooks" if [[ -f ".pre-commit-config.yaml" ]]; then @@ -196,6 +282,7 @@ setup_development_environment() { pre-commit install success "pre commit hooks installed" step "updating pre commit repos" + pre-commit autoupdate success "pre commit hooks updated" step "running pre commit on all files" @@ -203,7 +290,9 @@ setup_development_environment() { else warn ".pre-commit-config.yaml not found. skipping" fi +} +setup_secrets() { section "secret management setup" step "checking 1password cli" if has_cmd op; then @@ -222,7 +311,7 @@ setup_development_environment() { fi fi - # Ensure detect-secrets is available before using it + # Ensure detect-secrets is available before using it if ! command -v detect-secrets >/dev/null 2>&1; then python -m pip install detect-secrets fi @@ -236,17 +325,17 @@ setup_development_environment() { success "secrets baseline created" detect-secrets audit .secrets.baseline || warn "manual review of baseline recommended" success "secrets configuration verified" +} +verify_tools() { section "development tools verification" step "verifying development tools" for tool in black ruff mypy pytest beartype; do if python -m pip show "$tool" >/dev/null 2>&1; then success "$tool installed"; else warn "$tool missing"; fi done +} - section "project validation" - step "running final project validation" - info "checking imports" - +validate_project() { section "project validation" step "running final project validation" info "checking imports" @@ -258,7 +347,9 @@ for m in mods: importlib.import_module(m) print("โœ… All project modules import successfully") PYCODE +} +print_completion() { section "setup complete" echo -e "\n${GREEN}๐ŸŽ‰ development environment setup complete${RESET}\n" echo -e "${BLUE}installed:${RESET}" @@ -274,4 +365,17 @@ PYCODE echo " 3. start coding" } +setup_development_environment() { + check_system_requirements + setup_virtual_environment + setup_dependencies + configure_locale + setup_direnv + setup_pre_commit + setup_secrets + verify_tools + validate_project + print_completion +} + setup_development_environment From 68bc16ef59a3574078e4d225ebc041217fecdc33 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 21 Oct 2025 14:43:33 +0200 Subject: [PATCH 081/199] Add support for extra dependencies lockfile (requirements-full) - Add compiled requirements-full.txt for the "extra" extras set. - Wire extra lockfile into tooling: - Add pre-commit hook to verify the extra lockfile. - Update scripts/hooks/verify-lockfile.sh to recognize and compile the extra lockfile. - Enhance scripts/dependency.sh: - Default virtualenv to .venv. - Add EXTRA_LOCK env var and INSTALL_EXTRA flag. - Support --extra to compile and install the extra lockfile. - Print summary line for extra lockfile actions. - Regenerate lockfiles and update requirements-dev.txt and requirements.txt headers. - Minor pyproject.toml formatting cleanup. --- .pre-commit-config.yaml | 8 + pyproject.toml | 6 + requirements-dev.txt | 921 +++++++++++++++++-------------- requirements-full.txt | 32 ++ requirements.txt | 2 +- scripts/dependency.sh | 29 +- scripts/hooks/verify-lockfile.sh | 3 + 7 files changed, 573 insertions(+), 428 deletions(-) create mode 100644 requirements-full.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6fc88cf7..81fde4bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,6 +18,14 @@ repos: files: ^requirements-dev\.txt$ additional_dependencies: [pip-tools==7.5.0] + - id: verify-full-lockfile + name: Verify extra lockfile is up-to-date + entry: ./scripts/hooks/verify-lockfile.sh + args: ["requirements-full.txt"] + language: python + files: ^requirements-full\.txt$ + additional_dependencies: [pip-tools==7.5.0] + # Strip notebook outputs - id: nbstripout-autoadd name: nbstripout (auto-add notebooks) diff --git a/pyproject.toml b/pyproject.toml index 5f95c349..fad82ee9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,12 @@ dev = [ "rich", ] +# Add your extra dependencies here. These are custom packages not on PyPI. +# For example the 'zero-friction' package that needs to be installed via GitHub. +extra = [ + # "zero-friction @ git+https://github.com/TECHLETES/zero_friction.git@main" +] + [project.urls] Homepage = "https://github.com/TECHLETES/python_basic_template" Repository = "https://github.com/TECHLETES/python_basic_template" diff --git a/requirements-dev.txt b/requirements-dev.txt index 199fd81e..a80f8060 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,17 +2,17 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --cert=None --client-cert=None --extra=dev --generate-hashes --index-url=None --output-file=requirements-dev.txt --pip-args=None --strip-extras pyproject.toml +# pip-compile --extra=dev --generate-hashes --output-file=requirements-dev.txt --strip-extras pyproject.toml # -attrs==25.3.0 \ - --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \ - --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b +attrs==25.4.0 \ + --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ + --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 # via # jsonschema # referencing -beartype==0.21.0 \ - --hash=sha256:b6a1bd56c72f31b0a496a36cc55df6e2f475db166ad07fa4acc7e74f4c7f34c0 \ - --hash=sha256:f9a5078f5ce87261c2d22851d19b050b64f6a805439e8793aecf01ce660d3244 +beartype==0.22.2 \ + --hash=sha256:12077afe3528eba5c5b801f816712f7ff06f6da5509994c79561e29b48bcedb8 \ + --hash=sha256:ff3a7df26af8d15fa87f97934f0f6d41bbdadca971c410819104998dd26013d2 # via # pytest-beartype # python-basic-template (pyproject.toml) @@ -58,94 +58,128 @@ cachecontrol==0.14.3 \ # via # cachecontrol # pip-audit -certifi==2025.8.3 \ - --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ - --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 +certifi==2025.10.5 \ + --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ + --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 # via requests cfgv==3.4.0 \ --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 # via pre-commit -charset-normalizer==3.4.3 \ - --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ - --hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \ - --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ - --hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \ - --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ - --hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \ - --hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \ - --hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \ - --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ - --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ - --hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \ - --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ - --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ - --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ - --hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \ - --hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \ - --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ - --hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \ - --hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \ - --hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \ - --hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \ - --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ - --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ - --hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \ - --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ - --hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \ - --hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \ - --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ - --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ - --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ - --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ - --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ - --hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \ - --hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \ - --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ - --hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \ - --hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \ - --hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \ - --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ - --hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \ - --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ - --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ - --hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \ - --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ - --hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \ - --hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \ - --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ - --hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \ - --hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \ - --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ - --hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \ - --hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \ - --hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \ - --hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \ - --hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \ - --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ - --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ - --hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \ - --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ - --hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \ - --hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \ - --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ - --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ - --hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \ - --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ - --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ - --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ - --hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \ - --hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \ - --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ - --hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \ - --hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \ - --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ - --hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \ - --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ - --hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \ - --hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \ - --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ - --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 +charset-normalizer==3.4.4 \ + --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ + --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ + --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ + --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ + --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ + --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ + --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ + --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ + --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ + --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ + --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ + --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ + --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ + --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ + --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ + --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ + --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ + --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ + --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ + --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ + --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ + --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ + --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ + --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ + --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ + --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ + --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ + --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ + --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ + --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ + --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ + --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ + --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ + --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ + --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ + --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ + --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ + --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ + --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ + --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ + --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ + --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ + --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ + --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ + --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ + --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ + --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ + --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ + --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ + --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ + --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ + --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ + --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ + --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ + --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ + --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ + --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ + --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ + --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ + --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ + --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ + --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ + --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ + --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ + --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ + --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ + --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ + --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ + --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ + --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ + --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ + --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ + --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ + --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ + --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ + --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ + --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ + --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ + --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ + --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ + --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ + --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ + --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ + --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ + --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ + --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ + --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ + --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ + --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ + --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ + --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ + --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ + --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ + --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ + --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ + --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ + --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ + --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ + --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ + --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ + --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ + --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ + --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ + --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ + --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ + --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ + --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ + --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ + --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ + --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ + --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ + --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ + --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 # via requests click==8.3.0 \ --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ @@ -154,111 +188,99 @@ click==8.3.0 \ # black # flask # pip-tools -coverage==7.10.7 \ - --hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \ - --hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \ - --hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \ - --hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \ - --hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \ - --hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \ - --hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \ - --hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \ - --hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \ - --hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \ - --hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \ - --hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \ - --hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \ - --hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \ - --hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \ - --hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \ - --hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \ - --hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \ - --hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \ - --hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \ - --hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \ - --hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \ - --hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \ - --hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \ - --hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \ - --hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \ - --hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \ - --hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \ - --hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \ - --hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \ - --hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \ - --hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \ - --hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \ - --hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \ - --hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \ - --hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \ - --hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \ - --hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \ - --hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \ - --hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \ - --hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \ - --hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \ - --hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \ - --hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \ - --hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \ - --hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \ - --hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \ - --hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \ - --hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \ - --hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \ - --hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \ - --hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \ - --hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \ - --hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \ - --hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \ - --hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \ - --hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \ - --hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \ - --hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \ - --hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \ - --hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \ - --hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \ - --hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \ - --hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \ - --hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \ - --hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \ - --hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \ - --hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \ - --hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \ - --hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \ - --hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \ - --hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \ - --hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \ - --hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \ - --hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \ - --hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \ - --hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \ - --hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \ - --hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \ - --hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \ - --hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \ - --hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \ - --hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \ - --hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \ - --hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \ - --hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \ - --hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \ - --hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \ - --hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \ - --hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \ - --hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \ - --hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \ - --hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \ - --hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \ - --hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \ - --hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \ - --hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \ - --hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \ - --hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \ - --hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \ - --hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \ - --hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \ - --hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \ - --hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3 +coverage==7.11.0 \ + --hash=sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5 \ + --hash=sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e \ + --hash=sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab \ + --hash=sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a \ + --hash=sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82 \ + --hash=sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050 \ + --hash=sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893 \ + --hash=sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de \ + --hash=sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969 \ + --hash=sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601 \ + --hash=sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de \ + --hash=sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05 \ + --hash=sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3 \ + --hash=sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad \ + --hash=sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841 \ + --hash=sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847 \ + --hash=sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52 \ + --hash=sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11 \ + --hash=sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68 \ + --hash=sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73 \ + --hash=sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415 \ + --hash=sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745 \ + --hash=sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b \ + --hash=sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785 \ + --hash=sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4 \ + --hash=sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48 \ + --hash=sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2 \ + --hash=sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b \ + --hash=sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040 \ + --hash=sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0 \ + --hash=sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866 \ + --hash=sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1 \ + --hash=sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf \ + --hash=sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b \ + --hash=sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d \ + --hash=sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115 \ + --hash=sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2 \ + --hash=sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9 \ + --hash=sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43 \ + --hash=sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc \ + --hash=sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa \ + --hash=sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1 \ + --hash=sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088 \ + --hash=sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c \ + --hash=sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777 \ + --hash=sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623 \ + --hash=sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268 \ + --hash=sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186 \ + --hash=sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e \ + --hash=sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007 \ + --hash=sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7 \ + --hash=sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547 \ + --hash=sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3 \ + --hash=sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc \ + --hash=sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37 \ + --hash=sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721 \ + --hash=sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552 \ + --hash=sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca \ + --hash=sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497 \ + --hash=sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46 \ + --hash=sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2 \ + --hash=sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0 \ + --hash=sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44 \ + --hash=sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1 \ + --hash=sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4 \ + --hash=sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479 \ + --hash=sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e \ + --hash=sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2 \ + --hash=sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e \ + --hash=sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef \ + --hash=sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0 \ + --hash=sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad \ + --hash=sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075 \ + --hash=sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f \ + --hash=sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d \ + --hash=sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca \ + --hash=sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd \ + --hash=sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287 \ + --hash=sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f \ + --hash=sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0 \ + --hash=sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996 \ + --hash=sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d \ + --hash=sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd \ + --hash=sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31 \ + --hash=sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740 \ + --hash=sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591 \ + --hash=sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be \ + --hash=sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f \ + --hash=sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d \ + --hash=sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836 \ + --hash=sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d \ + --hash=sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c # via pytest-cov cyclonedx-python-lib==9.1.0 \ --hash=sha256:55693fca8edaecc3363b24af14e82cc6e659eb1e8353e58b587c42652ce0fb52 \ @@ -280,9 +302,9 @@ fastjsonschema==2.21.2 \ --hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \ --hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de # via nbformat -filelock==3.19.1 \ - --hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \ - --hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d +filelock==3.20.0 \ + --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ + --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 # via # cachecontrol # virtualenv @@ -290,17 +312,17 @@ flask==2.3.3 \ --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b # via python-basic-template (pyproject.toml) -identify==2.6.14 \ - --hash=sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e \ - --hash=sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a +identify==2.6.15 \ + --hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \ + --hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf # via pre-commit -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 # via requests -iniconfig==2.1.0 \ - --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ - --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 # via pytest itsdangerous==2.2.0 \ --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ @@ -318,9 +340,9 @@ jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -jupyter-core==5.8.1 \ - --hash=sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941 \ - --hash=sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0 +jupyter-core==5.9.1 \ + --hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \ + --hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 # via nbformat license-expression==30.4.4 \ --hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \ @@ -330,68 +352,96 @@ markdown-it-py==4.0.0 \ --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 # via rich -markupsafe==3.0.2 \ - --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \ - --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ - --hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \ - --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ - --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ - --hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \ - --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ - --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \ - --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ - --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \ - --hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \ - --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \ - --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \ - --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ - --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ - --hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \ - --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ - --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ - --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ - --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \ - --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \ - --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ - --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \ - --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \ - --hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \ - --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \ - --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \ - --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ - --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ - --hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \ - --hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \ - --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ - --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \ - --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \ - --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ - --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \ - --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \ - --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ - --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ - --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \ - --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ - --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ - --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ - --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \ - --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ - --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ - --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ - --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ - --hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \ - --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ - --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \ - --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \ - --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ - --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ - --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ - --hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \ - --hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \ - --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ - --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ - --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \ - --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50 +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via # jinja2 # werkzeug @@ -399,66 +449,69 @@ mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -msgpack==1.1.1 \ - --hash=sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8 \ - --hash=sha256:1abfc6e949b352dadf4bce0eb78023212ec5ac42f6abfd469ce91d783c149c2a \ - --hash=sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90 \ - --hash=sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf \ - --hash=sha256:2fbbc0b906a24038c9958a1ba7ae0918ad35b06cb449d398b76a7d08470b0ed9 \ - --hash=sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157 \ - --hash=sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed \ - --hash=sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d \ - --hash=sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0 \ - --hash=sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232 \ - --hash=sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084 \ - --hash=sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5 \ - --hash=sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1 \ - --hash=sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88 \ - --hash=sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752 \ - --hash=sha256:4d3237b224b930d58e9d83c81c0dba7aacc20fcc2f89c1e5423aa0529a4cd142 \ - --hash=sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac \ - --hash=sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef \ - --hash=sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323 \ - --hash=sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4 \ - --hash=sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458 \ - --hash=sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57 \ - --hash=sha256:61e35a55a546a1690d9d09effaa436c25ae6130573b6ee9829c37ef0f18d5e78 \ - --hash=sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd \ - --hash=sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69 \ - --hash=sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce \ - --hash=sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558 \ - --hash=sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd \ - --hash=sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2 \ - --hash=sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8 \ - --hash=sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0 \ - --hash=sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295 \ - --hash=sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c \ - --hash=sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26 \ - --hash=sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2 \ - --hash=sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f \ - --hash=sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4 \ - --hash=sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8 \ - --hash=sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9 \ - --hash=sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338 \ - --hash=sha256:996f2609ddf0142daba4cefd767d6db26958aac8439ee41db9cc0db9f4c4c3a6 \ - --hash=sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a \ - --hash=sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0 \ - --hash=sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a \ - --hash=sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478 \ - --hash=sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238 \ - --hash=sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7 \ - --hash=sha256:b8f93dcddb243159c9e4109c9750ba5b335ab8d48d9522c5308cd05d7e3ce600 \ - --hash=sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704 \ - --hash=sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a \ - --hash=sha256:bba1be28247e68994355e028dcd668316db30c1f758d3241a7b903ac78dcd285 \ - --hash=sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c \ - --hash=sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf \ - --hash=sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b \ - --hash=sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2 \ - --hash=sha256:da8f41e602574ece93dbbda1fab24650d6bf2a24089f9e9dbb4f5730ec1e58ad \ - --hash=sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b \ - --hash=sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b \ - --hash=sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75 +msgpack==1.1.2 \ + --hash=sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2 \ + --hash=sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014 \ + --hash=sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931 \ + --hash=sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b \ + --hash=sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b \ + --hash=sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999 \ + --hash=sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029 \ + --hash=sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0 \ + --hash=sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9 \ + --hash=sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c \ + --hash=sha256:350ad5353a467d9e3b126d8d1b90fe05ad081e2e1cef5753f8c345217c37e7b8 \ + --hash=sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f \ + --hash=sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a \ + --hash=sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42 \ + --hash=sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e \ + --hash=sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f \ + --hash=sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7 \ + --hash=sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb \ + --hash=sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef \ + --hash=sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf \ + --hash=sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245 \ + --hash=sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794 \ + --hash=sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af \ + --hash=sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff \ + --hash=sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e \ + --hash=sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296 \ + --hash=sha256:67016ae8c8965124fdede9d3769528ad8284f14d635337ffa6a713a580f6c030 \ + --hash=sha256:6bde749afe671dc44893f8d08e83bf475a1a14570d67c4bb5cec5573463c8833 \ + --hash=sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939 \ + --hash=sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa \ + --hash=sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90 \ + --hash=sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c \ + --hash=sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717 \ + --hash=sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406 \ + --hash=sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a \ + --hash=sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251 \ + --hash=sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2 \ + --hash=sha256:94fd7dc7d8cb0a54432f296f2246bc39474e017204ca6f4ff345941d4ed285a7 \ + --hash=sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e \ + --hash=sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b \ + --hash=sha256:9fba231af7a933400238cb357ecccf8ab5d51535ea95d94fc35b7806218ff844 \ + --hash=sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9 \ + --hash=sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87 \ + --hash=sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b \ + --hash=sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c \ + --hash=sha256:a8f6e7d30253714751aa0b0c84ae28948e852ee7fb0524082e6716769124bc23 \ + --hash=sha256:ad09b984828d6b7bb52d1d1d0c9be68ad781fa004ca39216c8a1e63c0f34ba3c \ + --hash=sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e \ + --hash=sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620 \ + --hash=sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69 \ + --hash=sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f \ + --hash=sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68 \ + --hash=sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27 \ + --hash=sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46 \ + --hash=sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa \ + --hash=sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00 \ + --hash=sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9 \ + --hash=sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84 \ + --hash=sha256:ea5405c46e690122a76531ab97a079e184c0daf491e588592d6a23d3e32af99e \ + --hash=sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20 \ + --hash=sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e \ + --hash=sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162 # via cachecontrol mypy==1.18.2 \ --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ @@ -549,13 +602,13 @@ pip-requirements-parser==32.0.1 \ --hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \ --hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3 # via pip-audit -pip-tools==7.5.0 \ - --hash=sha256:30639f50961bb09f49d22f4389e8d7d990709677c094ce1114186b1f2e9b5821 \ - --hash=sha256:69758e4e5a65f160e315d74db46246fdbb30d549f1ed0c4236d057122c9b0f18 +pip-tools==7.5.1 \ + --hash=sha256:a051a94794ba52df9acad2d7c9b0b09ae001617db458a543f8287fea7b89c2cf \ + --hash=sha256:f5ff803823529edc0e6e40c86b1aa7da7266fb1078093c8beea4e5b77877036a # via python-basic-template (pyproject.toml) -platformdirs==4.4.0 \ - --hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \ - --hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf +platformdirs==4.5.0 \ + --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ + --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 # via # black # jupyter-core @@ -606,70 +659,90 @@ pytest-cov==7.0.0 \ --hash=sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1 \ --hash=sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861 # via python-basic-template (pyproject.toml) -pytokens==0.1.10 \ - --hash=sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044 \ - --hash=sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b +pytokens==0.2.0 \ + --hash=sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43 \ + --hash=sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8 # via black -pyyaml==6.0.2 \ - --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ - --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ - --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ - --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ - --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ - --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ - --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ - --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ - --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ - --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ - --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ - --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ - --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ - --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ - --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ - --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ - --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ - --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ - --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ - --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ - --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ - --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ - --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ - --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ - --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ - --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ - --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ - --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ - --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ - --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ - --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ - --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ - --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ - --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ - --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ - --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ - --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ - --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ - --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ - --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ - --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ - --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ - --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ - --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ - --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ - --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ - --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ - --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ - --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ - --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ - --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ - --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ - --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via # detect-secrets # pre-commit -referencing==0.36.2 \ - --hash=sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa \ - --hash=sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0 +referencing==0.37.0 \ + --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ + --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 # via # jsonschema # jsonschema-specifications @@ -681,9 +754,9 @@ requests==2.32.5 \ # detect-secrets # pip-audit # python-basic-template (pyproject.toml) -rich==14.1.0 \ - --hash=sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f \ - --hash=sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8 +rich==14.2.0 \ + --hash=sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4 \ + --hash=sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd # via # pip-audit # python-basic-template (pyproject.toml) @@ -846,26 +919,26 @@ rpds-py==0.27.1 \ # via # jsonschema # referencing -ruff==0.13.1 \ - --hash=sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5 \ - --hash=sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700 \ - --hash=sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334 \ - --hash=sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0 \ - --hash=sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae \ - --hash=sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e \ - --hash=sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51 \ - --hash=sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a \ - --hash=sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0 \ - --hash=sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a \ - --hash=sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b \ - --hash=sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a \ - --hash=sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36 \ - --hash=sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c \ - --hash=sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae \ - --hash=sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317 \ - --hash=sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38 \ - --hash=sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783 \ - --hash=sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389 +ruff==0.14.1 \ + --hash=sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5 \ + --hash=sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b \ + --hash=sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020 \ + --hash=sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69 \ + --hash=sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345 \ + --hash=sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1 \ + --hash=sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd \ + --hash=sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61 \ + --hash=sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf \ + --hash=sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192 \ + --hash=sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6 \ + --hash=sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151 \ + --hash=sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d \ + --hash=sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5 \ + --hash=sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c \ + --hash=sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6 \ + --hash=sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44 \ + --hash=sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896 \ + --hash=sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224 # via python-basic-template (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ @@ -891,9 +964,9 @@ urllib3==2.5.0 \ --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc # via requests -virtualenv==20.34.0 \ - --hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \ - --hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a +virtualenv==20.35.3 \ + --hash=sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44 \ + --hash=sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a # via pre-commit werkzeug==3.1.3 \ --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ diff --git a/requirements-full.txt b/requirements-full.txt new file mode 100644 index 00000000..b86f25af --- /dev/null +++ b/requirements-full.txt @@ -0,0 +1,32 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --extra=extra --output-file=requirements-full.txt --strip-extras pyproject.toml +# +blinker==1.9.0 + # via flask +certifi==2025.10.5 + # via requests +charset-normalizer==3.4.4 + # via requests +click==8.3.0 + # via flask +flask==2.3.3 + # via python-basic-template (pyproject.toml) +idna==3.11 + # via requests +itsdangerous==2.2.0 + # via flask +jinja2==3.1.6 + # via flask +markupsafe==3.0.3 + # via + # jinja2 + # werkzeug +requests==2.32.5 + # via python-basic-template (pyproject.toml) +urllib3==2.5.0 + # via requests +werkzeug==3.1.3 + # via flask diff --git a/requirements.txt b/requirements.txt index 927b0d33..393647ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --cert=None --client-cert=None --generate-hashes --index-url=None --output-file=requirements.txt --pip-args=None --strip-extras pyproject.toml +# pip-compile --generate-hashes --output-file=requirements.txt --strip-extras pyproject.toml # blinker==1.9.0 \ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ diff --git a/scripts/dependency.sh b/scripts/dependency.sh index 50c69322..dfcfb86d 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -5,10 +5,11 @@ set -euo pipefail # Configuration # ----------------------------------------------------------------------------- # Use environment variables if they exist, otherwise set sensible defaults. -export VENV_DIR="${VENV_DIR:-venv}" +export VENV_DIR="${VENV_DIR:-.venv}" export PYPROJECT="${PYPROJECT:-pyproject.toml}" export PROD_LOCK="${PROD_LOCK:-requirements.txt}" export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" +export EXTRA_LOCK="${EXTRA_LOCK:-requirements-full.txt}" # ----------------------------------------------------------------------------- # Script Info @@ -17,9 +18,10 @@ export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" # Compiles lockfiles first, then installs, to ensure a robust workflow. # # Usage: -# ./scripts/dependency.sh # Compile and install both prod & dev +# ./scripts/dependency.sh # Compile and install both prod, dev & extra # ./scripts/dependency.sh --prod # Compile and install only prod # ./scripts/dependency.sh --dev # Compile and install only dev +# ./scripts/dependency.sh --extra # Compile and install only extra # ----------------------------------------------------------------------------- # UI Functions @@ -42,11 +44,13 @@ error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } # ----------------------------------------------------------------------------- INSTALL_PROD=false INSTALL_DEV=false +INSTALL_EXTRA=false # Default to both if no arguments are provided if [ $# -eq 0 ]; then INSTALL_PROD=true INSTALL_DEV=true + INSTALL_EXTRA=true else for arg in "$@"; do case $arg in @@ -56,8 +60,11 @@ else --dev) INSTALL_DEV=true ;; + --extra) + INSTALL_EXTRA=true + ;; *) - error "Unknown option: $arg. Usage: $0 [--prod] [--dev]" + error "Unknown option: $arg. Usage: $0 [--prod] [--dev] [--extra]" ;; esac done @@ -113,6 +120,15 @@ if [ "$INSTALL_DEV" = true ]; then --strip-extras fi +if [ "$INSTALL_EXTRA" = true ]; then + info "Compiling extra lockfile: '${EXTRA_LOCK}'" + pip-compile "${PYPROJECT}" \ + --extra=extra \ + --output-file="${EXTRA_LOCK}" \ + --strip-extras +fi + + success "All required lockfiles compiled." # ----------------------------------------------------------------------------- @@ -134,6 +150,12 @@ if [ "$INSTALL_DEV" = true ]; then pip install --require-hashes -r "${DEV_LOCK}" fi +if [ "$INSTALL_EXTRA" = true ]; then + info "Installing development dependencies from '${EXTRA_LOCK}'..." + # The extra requirements should include the prod ones, but `pip install` handles duplicates gracefully. + pip install -r "${EXTRA_LOCK}" +fi + success "All required dependencies installed." # ----------------------------------------------------------------------------- @@ -148,3 +170,4 @@ echo success "Dependency setup complete!" [ "$INSTALL_PROD" = true ] && echo " โ€ข Production dependencies updated from '${PROD_LOCK}'" [ "$INSTALL_DEV" = true ] && echo " โ€ข Development dependencies updated from '${DEV_LOCK}'" +[ "$INSTALL_EXTRA" = true ] && echo " โ€ข Extra dependencies updated from '${EXTRA_LOCK}'" diff --git a/scripts/hooks/verify-lockfile.sh b/scripts/hooks/verify-lockfile.sh index 34fc4ed0..40dc3e02 100755 --- a/scripts/hooks/verify-lockfile.sh +++ b/scripts/hooks/verify-lockfile.sh @@ -17,6 +17,9 @@ echo "Verifying integrity of '$LOCKFILE_TO_CHECK'..." if [[ "$LOCKFILE_TO_CHECK" == "requirements-dev.txt" ]]; then # Compile development dependencies pip-compile "$PYPROJECT_FILE" --extra=dev --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null +elif [[ "$LOCKFILE_TO_CHECK" == "requirements-full.txt" ]]; then + # Compile extra dependencies + pip-compile "$PYPROJECT_FILE" --extra=extra --output-file="$TEMP_LOCKFILE" --strip-extras --quiet > /dev/null else # Compile production dependencies by default pip-compile "$PYPROJECT_FILE" --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null From 9162f7dfcc17674a568b4de59c9180d353aea22b Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 21 Oct 2025 15:19:42 +0200 Subject: [PATCH 082/199] Add requirements-slim (core) lockfile and switch scripts/docs to layered dependency flow - Introduce SLIM_LOCK / requirements-slim across .envrc and setup.sh - Document layered dependency model in README (core/extra/dev), add --slim usage, and update file list and examples - Update scripts/dependency.sh: - Replace EXTRA_LOCK/requirements-full with SLIM_LOCK - Compile prod (core+extra), dev (core+extra+dev) and slim (core only) lockfiles - Adjust pip-compile flags and install behavior; default to dev install - Improve summary output - Update scripts/hooks/verify-lockfile.sh to validate requirements-slim and use matching pip-compile commands This adds a minimal "core" lockfile for slim deployments and aligns tooling and docs with the new layered dependency approach. --- .envrc | 1 + README.md | 96 +++++++++++++++++++++++--------- pyproject.toml | 14 ++++- scripts/dependency.sh | 90 +++++++++++++++--------------- scripts/hooks/verify-lockfile.sh | 14 ++--- scripts/setup.sh | 5 +- 6 files changed, 136 insertions(+), 84 deletions(-) diff --git a/.envrc b/.envrc index 79a2a22b..eea419c0 100644 --- a/.envrc +++ b/.envrc @@ -9,6 +9,7 @@ export PYTHONPATH=. export VENV_DIR=".venv" export PROD_LOCK="requirements.txt" export DEV_LOCK="requirements-dev.txt" +export SLIM_LOCK="requirements-slim.txt" export PYPROJECT="pyproject.toml" # Set a virtualenv automatically if venv exists diff --git a/README.md b/README.md index ddaf160e..42866e92 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,15 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co Why: it creates unpinned installs that differ per user and will break CI * Always declare packages in pyproject.toml only * Run the helper script to handle the package install for you - All dependencies + All dependencies (dev + extra + core) ./scripts/dependency.sh - Production only + Production dependencies (core + extra) ./scripts/dependency.sh --prod - Development only + Development dependencies only ./scripts/dependency.sh --dev -* After you change pyproject.toml run the script again. This will update requirements.txt and requirements-dev.txt automatically. + Core dependencies only (minimal) + ./scripts/dependency.sh --slim +* After you change pyproject.toml run the script again. This will update all requirement files automatically. ### Coding * Prefer small functions and clear modules so code is easy to test and reuse @@ -150,9 +152,10 @@ python_basic_template/ โ”‚ โ”œโ”€โ”€ secrets.py โ”‚ โ””โ”€โ”€ utils.py โ”œโ”€โ”€ tests/ # (Empty) Test directory scaffold -โ”œโ”€โ”€ pyproject.toml # Project configuration & dev dependencies -โ”œโ”€โ”€ requirements.txt # Compiled production dependencies -โ”œโ”€โ”€ requirements-dev.txt # Compiled dev dependencies +โ”œโ”€โ”€ pyproject.toml # Project configuration & all dependencies +โ”œโ”€โ”€ requirements-slim.txt # Compiled core dependencies only +โ”œโ”€โ”€ requirements.txt # Compiled production dependencies (core + extra) +โ”œโ”€โ”€ requirements-dev.txt # Compiled all dependencies (core + extra + dev) โ”œโ”€โ”€ .pre-commit-config.yaml # Pre-commit hooks config โ”œโ”€โ”€ .envrc # direnv environment config โ”œโ”€โ”€ .secrets.baseline # Secret detection baseline @@ -164,30 +167,68 @@ python_basic_template/ ## ๐Ÿ—๏ธ Dependency Management -This project uses a **hybrid dependency management approach**: +This project uses a **layered dependency management approach** with three distinct groups: +### Core Dependencies +- **Purpose**: Essential runtime requirements needed in all environments +- **Location**: `dependencies` section in `pyproject.toml` +- **Compiled to**: `requirements-slim.txt` +- **Examples**: Flask, requests, database drivers -### Production dependencies -- Declared under `dependencies` in `pyproject.toml` -- Locked to `requirements.txt` via pip-tools +### Extra Dependencies +- **Purpose**: Additional production features and optional components +- **Location**: `[project.optional-dependencies].extra` in `pyproject.toml` +- **Compiled to**: `requirements.txt` (core + extra) +- **Examples**: Private packages, optional integrations, performance libraries -### Development dependencies -- Declared under `[project.optional-dependencies].dev` in `pyproject.toml` -- Locked to `requirements-dev.txt` via pip-tools +### Development Dependencies +- **Purpose**: Development tools, testing frameworks, and code quality tools +- **Location**: `[project.optional-dependencies].dev` in `pyproject.toml` +- **Compiled to**: `requirements-dev.txt` (core + extra + dev) +- **Examples**: pytest, black, mypy, pre-commit -This setup ensures: - -- Clear separation of runtime vs. tooling packages -- Fully pinned, reproducible installs using lockfiles -- One declarative manifest (`pyproject.toml`) for all dependencies +This layered approach ensures: +- **Minimal deployments** with slim requirements for containers/lambdas +- **Full production** setup with all features enabled +- **Complete development** environment with all tooling +- **Reproducible builds** with fully pinned, hashed lockfiles ### Installation Options -| Command | What it installs | -|---------|------------------| -| `./scripts/dependency.sh --prod` | Production setup only | -| `./scripts/dependency.sh --dev` | Development setup only | -| `./scripts/dependency.sh` | Both production + development (Default) | +| Command | Dependencies Installed | Use Case | +|---------|----------------------|----------| +| `./scripts/dependency.sh --slim` | Core only | Minimal deployments, containers, lambdas | +| `./scripts/dependency.sh --prod` | Core + Extra | Full production environment | +| `./scripts/dependency.sh --dev` | Development only | CI/testing environments | +| `./scripts/dependency.sh` | Core + Extra + Dev | Complete development setup (Default) | + +### Adding Dependencies + +When adding new packages to your project, choose the appropriate section in `pyproject.toml`: + +```toml +# Core dependencies - Essential runtime requirements +dependencies = [ + "flask>=2.2.0,<3.0.0", # Web framework + "requests", # HTTP client + "sqlalchemy", # Database ORM +] + +# Extra dependencies - Optional production features +[project.optional-dependencies] +extra = [ + "redis", # Caching + "celery", # Task queue + "gunicorn", # Production server +] + +# Dev dependencies - Development and testing tools +dev = [ + "pytest>=7.0", # Testing framework + "black", # Code formatter + "mypy", # Type checker +] +``` **๐Ÿ“– Detailed guide**: [docs/2_dependency_management.md](docs/2_dependency_management.md) @@ -254,9 +295,10 @@ pytest tests/test_specific.py | File | Purpose | |------|---------| -| `pyproject.toml` | Project metadata, (dev) dependencies, tool config | -| `requirements.txt` | Compiled production dependencies (auto-generated) | -| `requirements-dev.txt` | Compiled development dependencies (auto-generated) | +| `pyproject.toml` | Project metadata, all dependencies, tool config | +| `requirements-slim.txt` | Compiled core dependencies only (auto-generated) | +| `requirements.txt` | Compiled production dependencies: core + extra (auto-generated) | +| `requirements-dev.txt` | Compiled all dependencies: core + extra + dev (auto-generated) | | `.pre-commit-config.yaml` | Pre-commit hooks configuration | | `.envrc` | Environment variables (direnv) | | `.secrets.baseline` | Secret detection baseline | diff --git a/pyproject.toml b/pyproject.toml index fad82ee9..7e013a92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,13 +24,20 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] -# Add project dependencies here. pip-tools will read them from here, do not use a requirements file! +# Core production dependencies required for the application to run. +# These are compiled into requirements-slim.txt (core only) and included in all other requirement files. +# Add only dependencies that are needed in production environments. dependencies = [ "flask>=2.2.0,<3.0.0", # Example dependency, replace with your own "requests" ] +# Optional dependency groups for different environments and use cases. +# These are used by pip-compile with --extra flags to generate different requirement files. [project.optional-dependencies] +# Development and testing dependencies. +# Includes code quality tools, testing frameworks, and development utilities. +# Compiled into requirements-dev.txt (core + extra + dev). dev = [ "pre-commit", "detect-secrets", @@ -47,8 +54,9 @@ dev = [ "rich", ] -# Add your extra dependencies here. These are custom packages not on PyPI. -# For example the 'zero-friction' package that needs to be installed via GitHub. +# Additional production dependencies. +# Includes private packages or optional features not in the core dependencies. +# Compiled into requirements.txt (core + extra). extra = [ # "zero-friction @ git+https://github.com/TECHLETES/zero_friction.git@main" ] diff --git a/scripts/dependency.sh b/scripts/dependency.sh index dfcfb86d..e1e568f7 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -9,7 +9,7 @@ export VENV_DIR="${VENV_DIR:-.venv}" export PYPROJECT="${PYPROJECT:-pyproject.toml}" export PROD_LOCK="${PROD_LOCK:-requirements.txt}" export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" -export EXTRA_LOCK="${EXTRA_LOCK:-requirements-full.txt}" +export SLIM_LOCK="${SLIM_LOCK:-requirements-slim.txt}" # ----------------------------------------------------------------------------- # Script Info @@ -18,10 +18,10 @@ export EXTRA_LOCK="${EXTRA_LOCK:-requirements-full.txt}" # Compiles lockfiles first, then installs, to ensure a robust workflow. # # Usage: -# ./scripts/dependency.sh # Compile and install both prod, dev & extra -# ./scripts/dependency.sh --prod # Compile and install only prod +# ./scripts/dependency.sh # Compile and install dev (which includes all) +# ./scripts/dependency.sh --prod # Compile and install only prod (full production) # ./scripts/dependency.sh --dev # Compile and install only dev -# ./scripts/dependency.sh --extra # Compile and install only extra +# ./scripts/dependency.sh --slim # Compile and install only slim (core only) # ----------------------------------------------------------------------------- # UI Functions @@ -44,13 +44,11 @@ error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } # ----------------------------------------------------------------------------- INSTALL_PROD=false INSTALL_DEV=false -INSTALL_EXTRA=false +INSTALL_SLIM=false -# Default to both if no arguments are provided +# Default to dev if no arguments are provided if [ $# -eq 0 ]; then - INSTALL_PROD=true INSTALL_DEV=true - INSTALL_EXTRA=true else for arg in "$@"; do case $arg in @@ -60,11 +58,11 @@ else --dev) INSTALL_DEV=true ;; - --extra) - INSTALL_EXTRA=true + --slim) + INSTALL_SLIM=true ;; *) - error "Unknown option: $arg. Usage: $0 [--prod] [--dev] [--extra]" + error "Unknown option: $arg. Usage: $0 [--prod] [--dev] [--slim]" ;; esac done @@ -103,33 +101,30 @@ fi step "PHASE 1: Compiling lockfiles from '${PYPROJECT}'..." -if [ "$INSTALL_PROD" = true ]; then - info "Compiling production lockfile: '${PROD_LOCK}'" - pip-compile "${PYPROJECT}" \ - --output-file="${PROD_LOCK}" \ - --generate-hashes \ - --strip-extras -fi +info "Compiling production lockfile: '${PROD_LOCK}'" +pip-compile "${PYPROJECT}" \ + --extra=extra \ + --output-file="${PROD_LOCK}" \ + --allow-unsafe \ + --generate-hashes \ + --strip-extras -if [ "$INSTALL_DEV" = true ]; then - info "Compiling development lockfile: '${DEV_LOCK}'" - pip-compile "${PYPROJECT}" \ - --extra=dev \ - --output-file="${DEV_LOCK}" \ - --generate-hashes \ - --strip-extras -fi +info "Compiling development lockfile: '${DEV_LOCK}'" +pip-compile "${PYPROJECT}" \ + --extra=dev \ + --extra=extra \ + --output-file="${DEV_LOCK}" \ + --allow-unsafe \ + --generate-hashes \ + --strip-extras -if [ "$INSTALL_EXTRA" = true ]; then - info "Compiling extra lockfile: '${EXTRA_LOCK}'" - pip-compile "${PYPROJECT}" \ - --extra=extra \ - --output-file="${EXTRA_LOCK}" \ - --strip-extras -fi +info "Compiling slim lockfile: '${SLIM_LOCK}'" +pip-compile "${PYPROJECT}" \ + --output-file="${SLIM_LOCK}" \ + --generate-hashes \ + --strip-extras - -success "All required lockfiles compiled." +success "All lockfiles compiled." # ----------------------------------------------------------------------------- # PHASE 2: INSTALL DEPENDENCIES FROM THE (NOW VERIFIED) LOCKFILES @@ -140,20 +135,18 @@ step "PHASE 2: Installing dependencies..." if [ "$INSTALL_PROD" = true ]; then info "Installing production dependencies from '${PROD_LOCK}'..." - # We use --require-hashes for a secure install. - pip install --require-hashes -r "${PROD_LOCK}" + pip install -r "${PROD_LOCK}" fi if [ "$INSTALL_DEV" = true ]; then info "Installing development dependencies from '${DEV_LOCK}'..." - # The dev requirements should include the prod ones, but `pip install` handles duplicates gracefully. - pip install --require-hashes -r "${DEV_LOCK}" + pip install -r "${DEV_LOCK}" fi -if [ "$INSTALL_EXTRA" = true ]; then - info "Installing development dependencies from '${EXTRA_LOCK}'..." - # The extra requirements should include the prod ones, but `pip install` handles duplicates gracefully. - pip install -r "${EXTRA_LOCK}" +if [ "$INSTALL_SLIM" = true ]; then + info "Installing slim dependencies from '${SLIM_LOCK}'..." + # The slim requirements are the core ones. + pip install --require-hashes -r "${SLIM_LOCK}" fi success "All required dependencies installed." @@ -168,6 +161,11 @@ success "Environment check passed." # --- Final Summary --- echo success "Dependency setup complete!" -[ "$INSTALL_PROD" = true ] && echo " โ€ข Production dependencies updated from '${PROD_LOCK}'" -[ "$INSTALL_DEV" = true ] && echo " โ€ข Development dependencies updated from '${DEV_LOCK}'" -[ "$INSTALL_EXTRA" = true ] && echo " โ€ข Extra dependencies updated from '${EXTRA_LOCK}'" +echo " โ€ข All lockfiles compiled" +if [ $# -eq 0 ]; then + echo " โ€ข Development dependencies (including all) installed from '${DEV_LOCK}'" +else + [ "$INSTALL_PROD" = true ] && echo " โ€ข Production dependencies installed from '${PROD_LOCK}'" + [ "$INSTALL_DEV" = true ] && echo " โ€ข Development dependencies installed from '${DEV_LOCK}'" + [ "$INSTALL_SLIM" = true ] && echo " โ€ข Slim dependencies installed from '${SLIM_LOCK}'" +fi diff --git a/scripts/hooks/verify-lockfile.sh b/scripts/hooks/verify-lockfile.sh index 40dc3e02..876db382 100755 --- a/scripts/hooks/verify-lockfile.sh +++ b/scripts/hooks/verify-lockfile.sh @@ -15,14 +15,14 @@ echo "Verifying integrity of '$LOCKFILE_TO_CHECK'..." # Determine which compile command to run based on the filename if [[ "$LOCKFILE_TO_CHECK" == "requirements-dev.txt" ]]; then - # Compile development dependencies - pip-compile "$PYPROJECT_FILE" --extra=dev --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null -elif [[ "$LOCKFILE_TO_CHECK" == "requirements-full.txt" ]]; then - # Compile extra dependencies - pip-compile "$PYPROJECT_FILE" --extra=extra --output-file="$TEMP_LOCKFILE" --strip-extras --quiet > /dev/null -else - # Compile production dependencies by default + # Compile development dependencies (core + extra + dev) + pip-compile "$PYPROJECT_FILE" --extra=dev --extra=extra --output-file="$TEMP_LOCKFILE" --generate-hashes --allow-unsafe --strip-extras --quiet > /dev/null +elif [[ "$LOCKFILE_TO_CHECK" == "requirements-slim.txt" ]]; then + # Compile core dependencies only pip-compile "$PYPROJECT_FILE" --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null +else + # Compile full production dependencies (core + extra) + pip-compile "$PYPROJECT_FILE" --extra=extra --output-file="$TEMP_LOCKFILE" --generate-hashes --allow-unsafe --strip-extras --quiet > /dev/null fi # Function to normalize a requirements file by removing the header comments diff --git a/scripts/setup.sh b/scripts/setup.sh index 58d72c7d..1f92b902 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -6,6 +6,7 @@ export VENV_DIR="${VENV_DIR:-.venv}" export PYPROJECT="${PYPROJECT:-pyproject.toml}" export PROD_LOCK="${PROD_LOCK:-requirements.txt}" export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" +export SLIM_LOCK="${SLIM_LOCK:-requirements-slim.txt}" # ui helpers GREEN='\033[0;32m' @@ -225,6 +226,7 @@ setup_dependencies() { section "hybrid dependency management" info "production deps in $PYPROJECT to $PROD_LOCK" info "development deps in $PYPROJECT extra [dev] to $DEV_LOCK" + info "slim deps in $PYPROJECT without extra [extra] to $SLIM_LOCK" step "installing pip tools" python -m pip install pip-tools build wheel success "pip tools installed" @@ -354,7 +356,8 @@ print_completion() { echo -e "\n${GREEN}๐ŸŽ‰ development environment setup complete${RESET}\n" echo -e "${BLUE}installed:${RESET}" echo " โ€ข $VENV_DIR/" - echo " โ€ข production deps ($PROD_LOCK)" + echo " โ€ข core production deps ($PROD_LOCK)" + echo " โ€ข slim production deps ($SLIM_LOCK)" echo " โ€ข dev tools ($DEV_LOCK)" echo " โ€ข pre commit hooks" echo " โ€ข secret baseline (.secrets.baseline)" From ed05de5957da300fe34aba7fcb610dce4af6f2c5 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 21 Oct 2025 15:38:33 +0200 Subject: [PATCH 083/199] Update dependency management docs to describe layered pip-tools workflow, slim/prod/dev lockfiles, and revised script steps --- README.md | 25 +++ docs/3_dependency_management.md | 261 ++++++++++++++++++++++++++----- pyproject.toml | 2 +- requirements-dev.txt | 8 +- requirements-full.txt | 32 ---- requirements-slim.txt | 254 ++++++++++++++++++++++++++++++ requirements.txt | 2 +- scripts/dependency.sh | 6 +- scripts/hooks/verify-lockfile.sh | 4 +- 9 files changed, 507 insertions(+), 87 deletions(-) delete mode 100644 requirements-full.txt create mode 100644 requirements-slim.txt diff --git a/README.md b/README.md index 42866e92..63a580df 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,31 @@ This layered approach ensures: | `./scripts/dependency.sh --dev` | Development only | CI/testing environments | | `./scripts/dependency.sh` | Core + Extra + Dev | Complete development setup (Default) | +### Security and Hash Verification + +This project uses **hash-based verification** for maximum security against supply chain attacks: + +- **Lockfiles include SHA256 hashes** for each dependency +- **Installation verifies packages** against these hashes +- **Prevents tampering** during the dependency supply chain + +#### When Hash Verification Fails + +Some packages **cannot be hashed** due to: +- Private packages from git repositories +- Local development packages (`file://` URLs) +- Packages with dynamic content + +**For these cases**, modify `scripts/dependency.sh`: +1. Remove `--require-hashes` from `pip install` commands +2. Remove `--generate-hashes` from corresponding `pip-compile` commands + +**โš ๏ธ Security Warning:** Only disable hashes for packages that genuinely cannot be hashed. Keep verification enabled wherever possible. + +#### The `--allow-unsafe` Flag + +All `pip-compile` commands include `--allow-unsafe` to handle packages with complex dependency trees that pip-tools might otherwise reject as "unsafe" due to unpinned dependencies or compatibility issues. + ### Adding Dependencies When adding new packages to your project, choose the appropriate section in `pyproject.toml`: diff --git a/docs/3_dependency_management.md b/docs/3_dependency_management.md index 97887350..b42e066e 100644 --- a/docs/3_dependency_management.md +++ b/docs/3_dependency_management.md @@ -1,98 +1,273 @@ -# 2. Dependency management +# 3. Dependency Management -This project uses a modern Python dependency management approach leveraging **pip-tools** with a single source of truth in **`pyproject.toml`**, while still generating fully pinned lockfiles for reproducible installs. +This project uses a **layered dependency management approach** leveraging **pip-tools** with a single source of truth in **`pyproject.toml`**, generating three distinct lockfiles for different deployment scenarios. --- -## 2.1 Workflow Overview +## 3.1 Dependency Layers Overview -- **Production dependencies** are declared under `dependencies` in `pyproject.toml` and locked to `requirements.txt` via pip-tools. -- **Development dependencies** are declared under `[project.optional-dependencies].dev` in `pyproject.toml` and locked to `requirements-dev.txt` via pip-tools. +The project organizes dependencies into three distinct layers: -This setup ensures: +### Core Dependencies +- **Purpose**: Essential runtime requirements needed in all environments +- **Location**: `dependencies` section in `pyproject.toml` +- **Compiled to**: `requirements-slim.txt` +- **Use cases**: Minimal deployments, containers, AWS Lambda functions +- **Examples**: Flask, requests, database drivers, core business logic libraries -- Clear separation of runtime vs. tooling packages -- Fully pinned, reproducible installs using lockfiles -- One declarative manifest (`pyproject.toml`) for all dependencies +### Extra Dependencies +- **Purpose**: Additional production features and optional components +- **Location**: `[project.optional-dependencies].extra` in `pyproject.toml` +- **Compiled to**: `requirements.txt` (core + extra) +- **Use cases**: Full production deployments with all features +- **Examples**: Redis clients, monitoring tools, performance libraries, private packages + +### Development Dependencies +- **Purpose**: Development tools, testing frameworks, and code quality tools +- **Location**: `[project.optional-dependencies].dev` in `pyproject.toml` +- **Compiled to**: `requirements-dev.txt` (core + extra + dev) +- **Use cases**: Local development, CI/CD pipelines, testing environments +- **Examples**: pytest, black, mypy, pre-commit, debugging tools + +This layered approach ensures: + +- **Minimal deployments** with only essential dependencies +- **Flexible production** setups with optional feature control +- **Complete development** environments with all tooling +- **Reproducible builds** with fully pinned, hashed lockfiles +- **Clear separation** between runtime, optional, and tooling packages --- -## 2.2 Using the helper script (`./scripts/dependency.sh`) +## 3.2 Using the helper script (`./scripts/dependency.sh`) -The provided script automates compilation and installation of both production and development dependencies. +The provided script automates compilation and installation across all dependency layers. ```bash -# Install both production and development dependencies +# Install all dependencies (core + extra + dev) - Default for development ./scripts/dependency.sh -# Install only production dependencies: +# Install core dependencies only - Minimal deployment +./scripts/dependency.sh --slim + +# Install production dependencies (core + extra) - Full production deployment ./scripts/dependency.sh --prod -# Install only development dependencies: +# Install development dependencies only - CI testing environments ./scripts/dependency.sh --dev ``` -### 2.2.1 Under the hood (manual steps) +### Installation Matrix + +| Command | Core | Extra | Dev | Use Case | +|---------|------|-------|-----|----------| +| `--slim` | โœ… | โŒ | โŒ | Containers, Lambda, minimal deployments | +| `--prod` | โœ… | โœ… | โŒ | Production servers with all features | +| `--dev` | โŒ | โŒ | โœ… | CI environments, testing only | +| (default) | โœ… | โœ… | โœ… | Local development, complete setup | + +### 3.2.1 Under the hood (manual steps) + +The script follows a two-phase approach: **compile all lockfiles first**, then **install selected dependencies**. -1. Activates the project virtual environment (`venv/bin/activate`). +#### Phase 1: Compile All Lockfiles + +1. Activates the project virtual environment (`.venv/bin/activate`). 2. Installs or verifies **pip-tools**. -3. Compiles the production lockfile from `pyproject.toml`: +3. Compiles the **slim lockfile** (core dependencies only): ```bash - pip-compile pyproject.toml --output-file=requirements.txt --generate-hashes + pip-compile pyproject.toml --output-file=requirements-slim.txt --generate-hashes --strip-extras --allow-unsafe ``` -4. Installs production packages: +4. Compiles the **production lockfile** (core + extra): ```bash - pip install --require-hashes -r requirements.txt + pip-compile pyproject.toml --extra=extra --output-file=requirements.txt --generate-hashes --strip-extras --allow-unsafe ``` -5. Compiles the development lockfile from the `dev` extras: +5. Compiles the **development lockfile** (core + extra + dev): ```bash - pip-compile pyproject.toml --extra=dev --output-file=requirements-dev.txt --generate-hashes + pip-compile pyproject.toml --extra=dev --extra=extra --output-file=requirements-dev.txt --generate-hashes --strip-extras --allow-unsafe ``` -6. Installs development packages: +#### Phase 2: Install Selected Dependencies - ```bash - pip install --require-hashes -r requirements-dev.txt - ``` +6. Installs packages based on the selected option: + - `--slim`: `pip install --require-hashes -r requirements-slim.txt` + - `--prod`: `pip install -r requirements.txt` + - `--dev`: `pip install -r requirements-dev.txt` + - (default): `pip install -r requirements-dev.txt` 7. Runs `pip check` to validate compatibility. +#### Why Two Phases? + +This approach ensures **lockfile consistency** before installation, preventing errors from manually edited or corrupted requirement files. + +### 3.2.2 Hash Requirements and Security + +By default, the script uses **hash-based verification** for maximum security: + +- **`--generate-hashes`** during compilation creates SHA256 hashes for each dependency +- **`--require-hashes`** during installation verifies packages against these hashes + +This prevents **supply chain attacks** by ensuring installed packages match exactly what was specified. + +#### When to Disable Hash Verification + +Some packages **cannot be hashed** due to: +- Private packages from git repositories +- Local development packages (`file://` URLs) +- Packages with dynamic content that changes between installs + +**For these cases, you can disable hash verification:** + +1. **In `scripts/dependency.sh`**, modify the relevant `pip install` commands: + - Remove `--require-hashes` from the install command + - Example: Change `pip install --require-hashes -r requirements-slim.txt` to `pip install -r requirements-slim.txt` + +2. **In `scripts/dependency.sh`**, modify the corresponding `pip-compile` commands: + - Remove `--generate-hashes` from the compile command + - Example: Change `pip-compile pyproject.toml --output-file=requirements-slim.txt --generate-hashes --strip-extras` to `pip-compile pyproject.toml --output-file=requirements-slim.txt --strip-extras` + +**โš ๏ธ Security Warning:** Only disable hashes for packages that genuinely cannot be hashed. Keep hash verification enabled wherever possible to maintain supply chain security. + +#### The `--allow-unsafe` Flag + +The `--allow-unsafe` flag is included in all `pip-compile` commands to handle packages that pip-tools considers "unsafe": + +- **Allows packages** that have unpinned dependencies or other issues +- **Enables compilation** of complex dependency trees that might otherwise fail +- **Required for many common packages** that have indirect dependencies with compatibility issues + +This flag is generally safe to use as it doesn't compromise security - it just allows pip-tools to work with packages that have dependency resolution challenges. + --- -## 2.3 Best Practices and Rules +## 3.3 Best Practices and Rules + +### Dependency Classification Guidelines + +**Core Dependencies** (`dependencies`): +- Database drivers (psycopg2, pymongo) +- Web frameworks (Flask, FastAPI) +- HTTP clients (requests, httpx) +- Essential business logic libraries +- Authentication libraries + +**Extra Dependencies** (`[project.optional-dependencies].extra`): +- Caching clients (redis, memcached) +- Message queues (celery, rq) +- Monitoring tools (sentry-sdk, prometheus-client) +- Performance libraries (uvloop, orjson) +- Private/internal packages + +**Dev Dependencies** (`[project.optional-dependencies].dev`): +- Testing frameworks (pytest, coverage) +- Code quality tools (black, ruff, mypy) +- Development utilities (pre-commit, rich) +- Documentation tools (sphinx, mkdocs) + +### Rules and Best Practices | โœ… Do This | โŒ Don't Do This | | --- | --- | -| Declare all deps in `pyproject.toml` | Scatter deps across multiple files | -| Use `./scripts/dependency.sh` or manual pip-compile commands | Run `pip install ` manually | -| Commit both `requirements.txt` and `requirements-dev.txt` lockfiles | Edit lockfiles by hand | -| Pin production installs via `requirements.txt` | Mix runtime and dev dependencies | -| Pin dev installs via `requirements-dev.txt` | Forget to regenerate lockfiles | +| Declare all deps in `pyproject.toml` only | Scatter deps across multiple files | +| Use `./scripts/dependency.sh` or manual pip-compile | Run `pip install ` manually | +| Commit all three lockfiles (`*requirements*.txt`) | Edit lockfiles by hand | +| Choose the right dependency layer | Mix runtime and tooling dependencies | +| Use `--slim` for minimal deployments | Use full lockfile for containers | +| Regenerate lockfiles after changing `pyproject.toml` | Forget to update lockfiles | | Run `pip check` after installation | Skip integrity verification | --- -## 2.4 Migration from Older Setups +## 3.4 Adding New Dependencies + +When adding packages, choose the appropriate section in `pyproject.toml`: + +```toml +# Core dependencies - Essential runtime requirements +# These go in ALL deployment scenarios +dependencies = [ + "flask>=2.2.0,<3.0.0", # Web framework + "requests>=2.28.0", # HTTP client + "sqlalchemy>=2.0.0", # Database ORM + "pydantic>=2.0.0", # Data validation +] + +[project.optional-dependencies] +# Extra dependencies - Optional production features +# These go in FULL production deployments only +extra = [ + "redis>=4.0.0", # Caching + "celery>=5.2.0", # Task queue + "sentry-sdk>=1.12.0", # Error monitoring + "gunicorn>=20.1.0", # Production WSGI server +] + +# Dev dependencies - Development and testing tools +# These go in DEVELOPMENT environments only +dev = [ + "pytest>=7.0", # Testing framework + "black>=22.0", # Code formatter + "mypy>=1.0", # Type checker + "pre-commit>=2.20.0", # Git hooks +] +``` + +### Decision Tree: Where to Put Dependencies -If migrating from a legacy `requirements.in` workflow: +``` +Is this package needed for the app to run? +โ”œโ”€ Yes: Is it absolutely essential? +โ”‚ โ”œโ”€ Yes โ†’ `dependencies` (core) +โ”‚ โ””โ”€ No โ†’ `[project.optional-dependencies].extra` +โ””โ”€ No: Is it for development/testing? + โ””โ”€ Yes โ†’ `[project.optional-dependencies].dev` +``` -1. **Move all runtime dependencies** into `[project.dependencies]` in `pyproject.toml`. -2. **Move all tooling dependencies** into `[project.optional-dependencies.dev]`. -3. **Remove** the old `requirements.in` file. -4. **Regenerate** lockfiles: +## 3.5 Migration from Older Setups - ```bash - pip-compile pyproject.toml --output-file=requirements.txt --generate-hashes - pip-compile pyproject.toml --extra=dev --output-file=requirements-dev.txt --generate-hashes - ``` +### From Legacy `requirements.in` Workflow: -5. **Install** with: +1. **Classify dependencies** into core, extra, and dev categories +2. **Move essential runtime deps** into `[project.dependencies]` +3. **Move optional production deps** into `[project.optional-dependencies].extra` +4. **Move tooling dependencies** into `[project.optional-dependencies].dev` +5. **Remove** old `requirements.in` files +6. **Regenerate** all lockfiles: ```bash ./scripts/dependency.sh ``` + +### From Two-File Setup (requirements.txt + requirements-dev.txt): + +1. **Audit existing requirements.txt** - split into core vs extra +2. **Move accordingly** to `dependencies` and `[project.optional-dependencies].extra` +3. **Move dev requirements** to `[project.optional-dependencies].dev` +4. **Regenerate** with the helper script + +## 3.6 Troubleshooting + +### Common Issues + +**Dependency conflicts between layers:** +```bash +pip check # Identify conflicts +# Review and resolve version constraints in pyproject.toml +``` + +**Lockfile out of sync:** +```bash +# Force regeneration of all lockfiles +./scripts/dependency.sh +``` + +**Package in wrong layer:** +- Check if a "dev" package is actually needed at runtime +- Move production packages from "dev" to "core" or "extra" +- Use the decision tree above to classify correctly diff --git a/pyproject.toml b/pyproject.toml index 7e013a92..a26a42b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dev = [ "beartype", "pytest-beartype", "pip-audit>=2.9.0", - "pip-tools", # for managing requirement*.txt + "pip-tools==7.5.0", # for managing requirement*.txt "rich", ] diff --git a/requirements-dev.txt b/requirements-dev.txt index a80f8060..bd0a844e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --extra=dev --generate-hashes --output-file=requirements-dev.txt --strip-extras pyproject.toml +# pip-compile --cert=None --client-cert=None --extra=dev --extra=extra --generate-hashes --index-url=None --output-file=requirements-dev.txt --pip-args=None --strip-extras pyproject.toml # attrs==25.4.0 \ --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ @@ -602,9 +602,9 @@ pip-requirements-parser==32.0.1 \ --hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \ --hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3 # via pip-audit -pip-tools==7.5.1 \ - --hash=sha256:a051a94794ba52df9acad2d7c9b0b09ae001617db458a543f8287fea7b89c2cf \ - --hash=sha256:f5ff803823529edc0e6e40c86b1aa7da7266fb1078093c8beea4e5b77877036a +pip-tools==7.5.0 \ + --hash=sha256:30639f50961bb09f49d22f4389e8d7d990709677c094ce1114186b1f2e9b5821 \ + --hash=sha256:69758e4e5a65f160e315d74db46246fdbb30d549f1ed0c4236d057122c9b0f18 # via python-basic-template (pyproject.toml) platformdirs==4.5.0 \ --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ diff --git a/requirements-full.txt b/requirements-full.txt deleted file mode 100644 index b86f25af..00000000 --- a/requirements-full.txt +++ /dev/null @@ -1,32 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile --extra=extra --output-file=requirements-full.txt --strip-extras pyproject.toml -# -blinker==1.9.0 - # via flask -certifi==2025.10.5 - # via requests -charset-normalizer==3.4.4 - # via requests -click==8.3.0 - # via flask -flask==2.3.3 - # via python-basic-template (pyproject.toml) -idna==3.11 - # via requests -itsdangerous==2.2.0 - # via flask -jinja2==3.1.6 - # via flask -markupsafe==3.0.3 - # via - # jinja2 - # werkzeug -requests==2.32.5 - # via python-basic-template (pyproject.toml) -urllib3==2.5.0 - # via requests -werkzeug==3.1.3 - # via flask diff --git a/requirements-slim.txt b/requirements-slim.txt new file mode 100644 index 00000000..750b23de --- /dev/null +++ b/requirements-slim.txt @@ -0,0 +1,254 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --cert=None --client-cert=None --generate-hashes --index-url=None --output-file=requirements-slim.txt --pip-args=None --strip-extras pyproject.toml +# +blinker==1.9.0 \ + --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ + --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc + # via flask +certifi==2025.10.5 \ + --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ + --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 + # via requests +charset-normalizer==3.4.4 \ + --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ + --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ + --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ + --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ + --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ + --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ + --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ + --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ + --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ + --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ + --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ + --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ + --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ + --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ + --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ + --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ + --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ + --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ + --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ + --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ + --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ + --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ + --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ + --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ + --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ + --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ + --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ + --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ + --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ + --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ + --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ + --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ + --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ + --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ + --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ + --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ + --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ + --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ + --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ + --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ + --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ + --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ + --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ + --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ + --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ + --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ + --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ + --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ + --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ + --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ + --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ + --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ + --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ + --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ + --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ + --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ + --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ + --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ + --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ + --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ + --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ + --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ + --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ + --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ + --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ + --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ + --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ + --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ + --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ + --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ + --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ + --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ + --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ + --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ + --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ + --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ + --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ + --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ + --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ + --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ + --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ + --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ + --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ + --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ + --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ + --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ + --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ + --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ + --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ + --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ + --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ + --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ + --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ + --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ + --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ + --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ + --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ + --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ + --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ + --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ + --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ + --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ + --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ + --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ + --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ + --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ + --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ + --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ + --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ + --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ + --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ + --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ + --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 + # via requests +click==8.3.0 \ + --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ + --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 + # via flask +flask==2.3.3 \ + --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ + --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b + # via python-basic-template (pyproject.toml) +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 + # via requests +itsdangerous==2.2.0 \ + --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ + --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 + # via flask +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via flask +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via + # jinja2 + # werkzeug +requests==2.32.5 \ + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf + # via python-basic-template (pyproject.toml) +urllib3==2.5.0 \ + --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ + --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc + # via requests +werkzeug==3.1.3 \ + --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ + --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 + # via flask diff --git a/requirements.txt b/requirements.txt index 393647ae..0d13a41f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --generate-hashes --output-file=requirements.txt --strip-extras pyproject.toml +# pip-compile --cert=None --client-cert=None --extra=extra --generate-hashes --index-url=None --output-file=requirements.txt --pip-args=None --strip-extras pyproject.toml # blinker==1.9.0 \ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ diff --git a/scripts/dependency.sh b/scripts/dependency.sh index e1e568f7..6e0a849f 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -105,7 +105,6 @@ info "Compiling production lockfile: '${PROD_LOCK}'" pip-compile "${PYPROJECT}" \ --extra=extra \ --output-file="${PROD_LOCK}" \ - --allow-unsafe \ --generate-hashes \ --strip-extras @@ -114,7 +113,6 @@ pip-compile "${PYPROJECT}" \ --extra=dev \ --extra=extra \ --output-file="${DEV_LOCK}" \ - --allow-unsafe \ --generate-hashes \ --strip-extras @@ -135,12 +133,12 @@ step "PHASE 2: Installing dependencies..." if [ "$INSTALL_PROD" = true ]; then info "Installing production dependencies from '${PROD_LOCK}'..." - pip install -r "${PROD_LOCK}" + pip install --require-hashes -r "${PROD_LOCK}" fi if [ "$INSTALL_DEV" = true ]; then info "Installing development dependencies from '${DEV_LOCK}'..." - pip install -r "${DEV_LOCK}" + pip install --require-hashes -r "${DEV_LOCK}" fi if [ "$INSTALL_SLIM" = true ]; then diff --git a/scripts/hooks/verify-lockfile.sh b/scripts/hooks/verify-lockfile.sh index 876db382..0b58f235 100755 --- a/scripts/hooks/verify-lockfile.sh +++ b/scripts/hooks/verify-lockfile.sh @@ -16,13 +16,13 @@ echo "Verifying integrity of '$LOCKFILE_TO_CHECK'..." # Determine which compile command to run based on the filename if [[ "$LOCKFILE_TO_CHECK" == "requirements-dev.txt" ]]; then # Compile development dependencies (core + extra + dev) - pip-compile "$PYPROJECT_FILE" --extra=dev --extra=extra --output-file="$TEMP_LOCKFILE" --generate-hashes --allow-unsafe --strip-extras --quiet > /dev/null + pip-compile "$PYPROJECT_FILE" --extra=dev --extra=extra --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null elif [[ "$LOCKFILE_TO_CHECK" == "requirements-slim.txt" ]]; then # Compile core dependencies only pip-compile "$PYPROJECT_FILE" --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null else # Compile full production dependencies (core + extra) - pip-compile "$PYPROJECT_FILE" --extra=extra --output-file="$TEMP_LOCKFILE" --generate-hashes --allow-unsafe --strip-extras --quiet > /dev/null + pip-compile "$PYPROJECT_FILE" --extra=extra --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null fi # Function to normalize a requirements file by removing the header comments From 7e8c418e957db1d94655b00611f41f103eef65ee Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 21 Oct 2025 15:40:57 +0200 Subject: [PATCH 084/199] Remove mention of --allow-unsafe --- README.md | 4 ---- docs/3_dependency_management.md | 16 +++------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 63a580df..fb330809 100644 --- a/README.md +++ b/README.md @@ -223,10 +223,6 @@ Some packages **cannot be hashed** due to: **โš ๏ธ Security Warning:** Only disable hashes for packages that genuinely cannot be hashed. Keep verification enabled wherever possible. -#### The `--allow-unsafe` Flag - -All `pip-compile` commands include `--allow-unsafe` to handle packages with complex dependency trees that pip-tools might otherwise reject as "unsafe" due to unpinned dependencies or compatibility issues. - ### Adding Dependencies When adding new packages to your project, choose the appropriate section in `pyproject.toml`: diff --git a/docs/3_dependency_management.md b/docs/3_dependency_management.md index b42e066e..714c9ac4 100644 --- a/docs/3_dependency_management.md +++ b/docs/3_dependency_management.md @@ -77,19 +77,19 @@ The script follows a two-phase approach: **compile all lockfiles first**, then * 3. Compiles the **slim lockfile** (core dependencies only): ```bash - pip-compile pyproject.toml --output-file=requirements-slim.txt --generate-hashes --strip-extras --allow-unsafe + pip-compile pyproject.toml --output-file=requirements-slim.txt --generate-hashes --strip-extras ``` 4. Compiles the **production lockfile** (core + extra): ```bash - pip-compile pyproject.toml --extra=extra --output-file=requirements.txt --generate-hashes --strip-extras --allow-unsafe + pip-compile pyproject.toml --extra=extra --output-file=requirements.txt --generate-hashes --strip-extras ``` 5. Compiles the **development lockfile** (core + extra + dev): ```bash - pip-compile pyproject.toml --extra=dev --extra=extra --output-file=requirements-dev.txt --generate-hashes --strip-extras --allow-unsafe + pip-compile pyproject.toml --extra=dev --extra=extra --output-file=requirements-dev.txt --generate-hashes --strip-extras ``` #### Phase 2: Install Selected Dependencies @@ -134,16 +134,6 @@ Some packages **cannot be hashed** due to: **โš ๏ธ Security Warning:** Only disable hashes for packages that genuinely cannot be hashed. Keep hash verification enabled wherever possible to maintain supply chain security. -#### The `--allow-unsafe` Flag - -The `--allow-unsafe` flag is included in all `pip-compile` commands to handle packages that pip-tools considers "unsafe": - -- **Allows packages** that have unpinned dependencies or other issues -- **Enables compilation** of complex dependency trees that might otherwise fail -- **Required for many common packages** that have indirect dependencies with compatibility issues - -This flag is generally safe to use as it doesn't compromise security - it just allows pip-tools to work with packages that have dependency resolution challenges. - --- ## 3.3 Best Practices and Rules From 92b1dff756caa29443bf4ffdf7940e2a28452420 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 22 Oct 2025 08:37:54 +0200 Subject: [PATCH 085/199] Refactor dependency installation instructions in READMEU Consolidate dependency installation instructions and clarify usage for different environments. --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fb330809..b2c3ee72 100644 --- a/README.md +++ b/README.md @@ -21,22 +21,24 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co Why: it creates unpinned installs that differ per user and will break CI * Always declare packages in pyproject.toml only * Run the helper script to handle the package install for you - All dependencies (dev + extra + core) - ./scripts/dependency.sh - Production dependencies (core + extra) - ./scripts/dependency.sh --prod - Development dependencies only - ./scripts/dependency.sh --dev - Core dependencies only (minimal) - ./scripts/dependency.sh --slim +- All dependencies (dev + extra + core): `./scripts/dependency.sh` or `./scripts/dependency.sh --dev` + - Install this locally for developement +- Production dependencies (core + extra): `./scripts/dependency.sh --prod` + - Install this in staging or production environments. It contains all needed to run the project without dev tools. +- Core dependencies only (minimal): `./scripts/dependency.sh --slim` + - Use this in CI pipelines. It contains the core dependencies to run the project, nothing more. + * After you change pyproject.toml run the script again. This will update all requirement files automatically. +* If you remove dependencies from pyproject.toml that are no longeer needed, delete your virtualenvironment and run the script. ### Coding * Prefer small functions and clear modules so code is easy to test and reuse * Use type hints to show expected inputs and outputs Example + ``` def add(a: int, b: int) -> int: return a + b + ``` Why: your editor can catch mistakes early and mypy can check types automatically * Put shared helpers in utils * Use jupyter notebooks only for small tests / development. From c92428de6852f9094977772b9e80b30fb8b49e3a Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 22 Oct 2025 12:33:24 +0200 Subject: [PATCH 086/199] Add PR template --- .github/pull_request_template.md | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..44efa76c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,43 @@ +### Description + +_Briefly describe what this PR aims to solve. Include background context that will help reviewers understand the purpose of the PR._ + +### Type of change + +- [ ] Bug Fix (non-breaking change which fixes an issue) +- [ ] New Feature (non-breaking change which adds functionality) +- [ ] Documentation Update +- [ ] Refactoring +- [ ] Performance Improvement +- [ ] Breaking Change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Other (please describe): + +### Checklist + +- [ ] My code follows the project's style guidelines +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules + +### Security Considerations + +- [ ] I have reviewed the security implications of my changes +- [ ] I have checked for potential vulnerabilities in dependencies +- [ ] I have ensured no sensitive information is exposed +- [ ] I have considered the impact on data privacy + +### Testing + +_Describe the testing performed to validate your changes. Include details on how to reproduce the tests._ + +### Screenshots (if applicable) + +_Add screenshots to help explain your changes, especially for UI-related changes._ + +### Related Issues + +_Closes #issue_number_ From 2f3ee2e9e9c7a106e444913b89aae83c2df232be Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 27 Oct 2025 14:53:14 +0100 Subject: [PATCH 087/199] Move issues template folder to correct location --- .github/{workflows => }/ISSUE_TEMPLATE/bug-report.md | 0 .github/{workflows => }/ISSUE_TEMPLATE/config.yml | 0 .github/{workflows => }/ISSUE_TEMPLATE/docs_update.md | 0 .github/{workflows => }/ISSUE_TEMPLATE/feature-request.md | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename .github/{workflows => }/ISSUE_TEMPLATE/bug-report.md (100%) rename .github/{workflows => }/ISSUE_TEMPLATE/config.yml (100%) rename .github/{workflows => }/ISSUE_TEMPLATE/docs_update.md (100%) rename .github/{workflows => }/ISSUE_TEMPLATE/feature-request.md (100%) diff --git a/.github/workflows/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md similarity index 100% rename from .github/workflows/ISSUE_TEMPLATE/bug-report.md rename to .github/ISSUE_TEMPLATE/bug-report.md diff --git a/.github/workflows/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/workflows/ISSUE_TEMPLATE/config.yml rename to .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/workflows/ISSUE_TEMPLATE/docs_update.md b/.github/ISSUE_TEMPLATE/docs_update.md similarity index 100% rename from .github/workflows/ISSUE_TEMPLATE/docs_update.md rename to .github/ISSUE_TEMPLATE/docs_update.md diff --git a/.github/workflows/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md similarity index 100% rename from .github/workflows/ISSUE_TEMPLATE/feature-request.md rename to .github/ISSUE_TEMPLATE/feature-request.md From dd281003aa7c9c012f0d2d66b52705e9ffbe7475 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 13 Jan 2026 12:04:37 +0100 Subject: [PATCH 088/199] Update dependencies and remove obsolete scripts - Updated `requirements.txt` to reflect new versions of dependencies: - `certifi` upgraded from 2025.10.5 to 2026.1.4 - `click` upgraded from 8.3.0 to 8.3.1 - Added `filelock` version 3.20.3 - `urllib3` upgraded from 2.5.0 to 2.6.3 - `werkzeug` upgraded from 3.1.3 to 3.1.5 - Removed obsolete PowerShell scripts: - `dependency.ps1` - `env.ps1` - `new-branch.ps1` - `setup.ps1` - Updated `dependency.sh` to handle optional dependencies and improve lockfile compilation logic. --- pyproject.toml | 5 +- requirements-dev.txt | 910 ++++++++++++++----------- requirements-slim.txt | 254 ------- requirements.txt | 38 +- scripts/dependency.ps1 | 74 -- scripts/dependency.sh | 76 ++- scripts/env.ps1 | 25 - scripts/hooks/nbstripout-install.ps1 | 14 - scripts/hooks/precommit_pytest.ps1 | 3 - scripts/hooks/verify-dev-lockfile.ps1 | 3 - scripts/hooks/verify-lockfile.ps1 | 43 -- scripts/hooks/verify-prod-lockfile.ps1 | 3 - scripts/new-branch.ps1 | 52 -- scripts/setup.ps1 | 75 -- 14 files changed, 588 insertions(+), 987 deletions(-) delete mode 100644 requirements-slim.txt delete mode 100644 scripts/dependency.ps1 delete mode 100644 scripts/env.ps1 delete mode 100644 scripts/hooks/nbstripout-install.ps1 delete mode 100644 scripts/hooks/precommit_pytest.ps1 delete mode 100644 scripts/hooks/verify-dev-lockfile.ps1 delete mode 100644 scripts/hooks/verify-lockfile.ps1 delete mode 100644 scripts/hooks/verify-prod-lockfile.ps1 delete mode 100644 scripts/new-branch.ps1 delete mode 100644 scripts/setup.ps1 diff --git a/pyproject.toml b/pyproject.toml index a26a42b8..40c4d7ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,10 @@ classifiers = [ # Add only dependencies that are needed in production environments. dependencies = [ "flask>=2.2.0,<3.0.0", # Example dependency, replace with your own - "requests" + "requests", + "urllib3>=2.6.3,<3.0.0", + "werkzeug>=3.1.5,<4.0.0", + "filelock>=3.20.1,<4.0.0", ] # Optional dependency groups for different environments and use cases. diff --git a/requirements-dev.txt b/requirements-dev.txt index bd0a844e..db5b9d50 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --cert=None --client-cert=None --extra=dev --extra=extra --generate-hashes --index-url=None --output-file=requirements-dev.txt --pip-args=None --strip-extras pyproject.toml +# pip-compile --cert=None --client-cert=None --extra=dev --generate-hashes --index-url=None --output-file=requirements-dev.txt --pip-args=None --strip-extras pyproject.toml # attrs==25.4.0 \ --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ @@ -10,35 +10,40 @@ attrs==25.4.0 \ # via # jsonschema # referencing -beartype==0.22.2 \ - --hash=sha256:12077afe3528eba5c5b801f816712f7ff06f6da5509994c79561e29b48bcedb8 \ - --hash=sha256:ff3a7df26af8d15fa87f97934f0f6d41bbdadca971c410819104998dd26013d2 +beartype==0.22.9 \ + --hash=sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f \ + --hash=sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2 # via # pytest-beartype # python-basic-template (pyproject.toml) -black==25.9.0 \ - --hash=sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175 \ - --hash=sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619 \ - --hash=sha256:154b06d618233fe468236ba1f0e40823d4eb08b26f5e9261526fde34916b9140 \ - --hash=sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0 \ - --hash=sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92 \ - --hash=sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f \ - --hash=sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa \ - --hash=sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae \ - --hash=sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a \ - --hash=sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357 \ - --hash=sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4 \ - --hash=sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e \ - --hash=sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d \ - --hash=sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608 \ - --hash=sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831 \ - --hash=sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f \ - --hash=sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7 \ - --hash=sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1 \ - --hash=sha256:e3c1f4cd5e93842774d9ee4ef6cd8d17790e65f44f7cdbaab5f2cf8ccf22a823 \ - --hash=sha256:e593466de7b998374ea2585a471ba90553283fb9beefcfa430d84a2651ed5933 \ - --hash=sha256:ef69351df3c84485a8beb6f7b8f9721e2009e20ef80a8d619e2d1788b7816d47 \ - --hash=sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713 +black==25.12.0 \ + --hash=sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783 \ + --hash=sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f \ + --hash=sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5 \ + --hash=sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf \ + --hash=sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43 \ + --hash=sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea \ + --hash=sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be \ + --hash=sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f \ + --hash=sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d \ + --hash=sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892 \ + --hash=sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a \ + --hash=sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828 \ + --hash=sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655 \ + --hash=sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a \ + --hash=sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b \ + --hash=sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7 \ + --hash=sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f \ + --hash=sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83 \ + --hash=sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f \ + --hash=sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5 \ + --hash=sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5 \ + --hash=sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da \ + --hash=sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce \ + --hash=sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59 \ + --hash=sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a \ + --hash=sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b \ + --hash=sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8 # via python-basic-template (pyproject.toml) blinker==1.9.0 \ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ @@ -48,23 +53,23 @@ boolean-py==5.0 \ --hash=sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95 \ --hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9 # via license-expression -build==1.3.0 \ - --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ - --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 +build==1.4.0 \ + --hash=sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596 \ + --hash=sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936 # via pip-tools -cachecontrol==0.14.3 \ - --hash=sha256:73e7efec4b06b20d9267b441c1f733664f989fb8688391b670ca812d70795d11 \ - --hash=sha256:b35e44a3113f17d2a31c1e6b27b9de6d4405f84ae51baa8c1d3cc5b633010cae +cachecontrol==0.14.4 \ + --hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b \ + --hash=sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1 # via # cachecontrol # pip-audit -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +certifi==2026.1.4 \ + --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ + --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 # via requests -cfgv==3.4.0 \ - --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ - --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 +cfgv==3.5.0 \ + --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ + --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 # via pre-commit charset-normalizer==3.4.4 \ --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ @@ -181,110 +186,110 @@ charset-normalizer==3.4.4 \ --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 # via requests -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.3.1 \ + --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ + --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 # via # black # flask # pip-tools -coverage==7.11.0 \ - --hash=sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5 \ - --hash=sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e \ - --hash=sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab \ - --hash=sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a \ - --hash=sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82 \ - --hash=sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050 \ - --hash=sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893 \ - --hash=sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de \ - --hash=sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969 \ - --hash=sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601 \ - --hash=sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de \ - --hash=sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05 \ - --hash=sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3 \ - --hash=sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad \ - --hash=sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841 \ - --hash=sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847 \ - --hash=sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52 \ - --hash=sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11 \ - --hash=sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68 \ - --hash=sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73 \ - --hash=sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415 \ - --hash=sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745 \ - --hash=sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b \ - --hash=sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785 \ - --hash=sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4 \ - --hash=sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48 \ - --hash=sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2 \ - --hash=sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b \ - --hash=sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040 \ - --hash=sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0 \ - --hash=sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866 \ - --hash=sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1 \ - --hash=sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf \ - --hash=sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b \ - --hash=sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d \ - --hash=sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115 \ - --hash=sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2 \ - --hash=sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9 \ - --hash=sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43 \ - --hash=sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc \ - --hash=sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa \ - --hash=sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1 \ - --hash=sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088 \ - --hash=sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c \ - --hash=sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777 \ - --hash=sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623 \ - --hash=sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268 \ - --hash=sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186 \ - --hash=sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e \ - --hash=sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007 \ - --hash=sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7 \ - --hash=sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547 \ - --hash=sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3 \ - --hash=sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc \ - --hash=sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37 \ - --hash=sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721 \ - --hash=sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552 \ - --hash=sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca \ - --hash=sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497 \ - --hash=sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46 \ - --hash=sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2 \ - --hash=sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0 \ - --hash=sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44 \ - --hash=sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1 \ - --hash=sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4 \ - --hash=sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479 \ - --hash=sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e \ - --hash=sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2 \ - --hash=sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e \ - --hash=sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef \ - --hash=sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0 \ - --hash=sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad \ - --hash=sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075 \ - --hash=sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f \ - --hash=sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d \ - --hash=sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca \ - --hash=sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd \ - --hash=sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287 \ - --hash=sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f \ - --hash=sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0 \ - --hash=sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996 \ - --hash=sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d \ - --hash=sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd \ - --hash=sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31 \ - --hash=sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740 \ - --hash=sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591 \ - --hash=sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be \ - --hash=sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f \ - --hash=sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d \ - --hash=sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836 \ - --hash=sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d \ - --hash=sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c +coverage==7.13.1 \ + --hash=sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784 \ + --hash=sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e \ + --hash=sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3 \ + --hash=sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0 \ + --hash=sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79 \ + --hash=sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75 \ + --hash=sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6 \ + --hash=sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f \ + --hash=sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88 \ + --hash=sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b \ + --hash=sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573 \ + --hash=sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0 \ + --hash=sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2 \ + --hash=sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0 \ + --hash=sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba \ + --hash=sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd \ + --hash=sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c \ + --hash=sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673 \ + --hash=sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e \ + --hash=sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee \ + --hash=sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3 \ + --hash=sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e \ + --hash=sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461 \ + --hash=sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6 \ + --hash=sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6 \ + --hash=sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500 \ + --hash=sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a \ + --hash=sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9 \ + --hash=sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc \ + --hash=sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5 \ + --hash=sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3 \ + --hash=sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d \ + --hash=sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842 \ + --hash=sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1 \ + --hash=sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486 \ + --hash=sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29 \ + --hash=sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968 \ + --hash=sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9 \ + --hash=sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4 \ + --hash=sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c \ + --hash=sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a \ + --hash=sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465 \ + --hash=sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8 \ + --hash=sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09 \ + --hash=sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416 \ + --hash=sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894 \ + --hash=sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a \ + --hash=sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9 \ + --hash=sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4 \ + --hash=sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5 \ + --hash=sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f \ + --hash=sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb \ + --hash=sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd \ + --hash=sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae \ + --hash=sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a \ + --hash=sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e \ + --hash=sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564 \ + --hash=sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd \ + --hash=sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6 \ + --hash=sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f \ + --hash=sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7 \ + --hash=sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a \ + --hash=sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee \ + --hash=sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62 \ + --hash=sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d \ + --hash=sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef \ + --hash=sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b \ + --hash=sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1 \ + --hash=sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78 \ + --hash=sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398 \ + --hash=sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53 \ + --hash=sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d \ + --hash=sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c \ + --hash=sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909 \ + --hash=sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f \ + --hash=sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9 \ + --hash=sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90 \ + --hash=sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851 \ + --hash=sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147 \ + --hash=sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf \ + --hash=sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864 \ + --hash=sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4 \ + --hash=sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf \ + --hash=sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4 \ + --hash=sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a \ + --hash=sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4 \ + --hash=sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c \ + --hash=sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992 \ + --hash=sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c \ + --hash=sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19 \ + --hash=sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7 \ + --hash=sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766 # via pytest-cov -cyclonedx-python-lib==9.1.0 \ - --hash=sha256:55693fca8edaecc3363b24af14e82cc6e659eb1e8353e58b587c42652ce0fb52 \ - --hash=sha256:86935f2c88a7b47a529b93c724dbd3e903bc573f6f8bd977628a7ca1b5dadea1 +cyclonedx-python-lib==11.6.0 \ + --hash=sha256:7fb85a4371fa3a203e5be577ac22b7e9a7157f8b0058b7448731474d6dea7bf0 \ + --hash=sha256:94f4aae97db42a452134dafdddcfab9745324198201c4777ed131e64c8380759 # via pip-audit defusedxml==0.7.1 \ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ @@ -302,19 +307,20 @@ fastjsonschema==2.21.2 \ --hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \ --hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de # via nbformat -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 +filelock==3.20.3 \ + --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ + --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 # via # cachecontrol + # python-basic-template (pyproject.toml) # virtualenv flask==2.3.3 \ --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b # via python-basic-template (pyproject.toml) -identify==2.6.15 \ - --hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \ - --hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf +identify==2.6.16 \ + --hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \ + --hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980 # via pre-commit idna==3.11 \ --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ @@ -332,9 +338,9 @@ jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via flask -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce # via nbformat jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ @@ -344,6 +350,84 @@ jupyter-core==5.9.1 \ --hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \ --hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 # via nbformat +librt==0.7.7 \ + --hash=sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7 \ + --hash=sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832 \ + --hash=sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848 \ + --hash=sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc \ + --hash=sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91 \ + --hash=sha256:14ef0f4ac3728ffd85bfc58e2f2f48fb4ef4fa871876f13a73a7381d10a9f77c \ + --hash=sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630 \ + --hash=sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b \ + --hash=sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977 \ + --hash=sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8 \ + --hash=sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2 \ + --hash=sha256:264720fc288c86039c091a4ad63419a5d7cabbf1c1c9933336a957ed2483e570 \ + --hash=sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b \ + --hash=sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc \ + --hash=sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab \ + --hash=sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456 \ + --hash=sha256:44d63ce643f34a903f09ff7ca355aae019a3730c7afd6a3c037d569beeb5d151 \ + --hash=sha256:467dbd7443bda08338fc8ad701ed38cef48194017554f4c798b0a237904b3f99 \ + --hash=sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949 \ + --hash=sha256:4bf3cc46d553693382d2abf5f5bd493d71bb0f50a7c0beab18aa13a5545c8900 \ + --hash=sha256:4dcee2f921a8632636d1c37f1bbdb8841d15666d119aa61e5399c5268e7ce02e \ + --hash=sha256:50d1d1ee813d2d1a3baf2873634ba506b263032418d16287c92ec1cc9c1a00cb \ + --hash=sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d \ + --hash=sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111 \ + --hash=sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398 \ + --hash=sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259 \ + --hash=sha256:6ae8aec43117a645a31e5f60e9e3a0797492e747823b9bda6972d521b436b4e8 \ + --hash=sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d \ + --hash=sha256:7b16ccaeff0ed4355dfb76fe1ea7a5d6d03b5ad27f295f77ee0557bc20a72495 \ + --hash=sha256:7d13cc340b3b82134f8038a2bfe7137093693dcad8ba5773da18f95ad6b77a8a \ + --hash=sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a \ + --hash=sha256:8185c8497d45164e256376f9da5aed2bb26ff636c798c9dabe313b90e9f25b28 \ + --hash=sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c \ + --hash=sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b \ + --hash=sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6 \ + --hash=sha256:8f2f8dcf5ab9f80fb970c6fd780b398efb2f50c1962485eb8d3ab07788595a48 \ + --hash=sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244 \ + --hash=sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d \ + --hash=sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf \ + --hash=sha256:983de36b5a83fe9222f4f7dcd071f9b1ac6f3f17c0af0238dadfb8229588f890 \ + --hash=sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45 \ + --hash=sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811 \ + --hash=sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff \ + --hash=sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c \ + --hash=sha256:a1f5cc41a570269d1be7a676655875e3a53de4992a9fa38efb7983e97cf73d7c \ + --hash=sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba \ + --hash=sha256:a487b71fbf8a9edb72a8c7a456dda0184642d99cd007bc819c0b7ab93676a8ee \ + --hash=sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee \ + --hash=sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029 \ + --hash=sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d \ + --hash=sha256:ab2a2a9cd7d044e1a11ca64a86ad3361d318176924bbe5152fbc69f99be20b8c \ + --hash=sha256:ad3fc2d859a709baf9dd9607bb72f599b1cfb8a39eafd41307d0c3c4766763cb \ + --hash=sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1 \ + --hash=sha256:aea05f701ccd2a76b34f0daf47ca5068176ff553510b614770c90d76ac88df06 \ + --hash=sha256:b8bb331aad734b059c4b450cd0a225652f16889e286b2345af5e2c3c625c3d85 \ + --hash=sha256:bdb9f3d865b2dafe7f9ad7f30ef563c80d0ddd2fdc8cc9b8e4f242f475e34d75 \ + --hash=sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536 \ + --hash=sha256:c48c7e150c095d5e3cea7452347ba26094be905d6099d24f9319a8b475fcd3e0 \ + --hash=sha256:c7e5070cf3ec92d98f57574da0224f8c73faf1ddd6d8afa0b8c9f6e86997bc74 \ + --hash=sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd \ + --hash=sha256:c8ffe3431d98cc043a14e88b21288b5ec7ee12cb01260e94385887f285ef9389 \ + --hash=sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe \ + --hash=sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3 \ + --hash=sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804 \ + --hash=sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503 \ + --hash=sha256:e40d20ae1722d6b8ea6acf4597e789604649dcd9c295eb7361a28225bc2e9e12 \ + --hash=sha256:e4836c5645f40fbdc275e5670819bde5ab5f2e882290d304e3c6ddab1576a6d0 \ + --hash=sha256:e4ab69fa37f8090f2d971a5d2bc606c7401170dbdae083c393d6cbf439cb45b8 \ + --hash=sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50 \ + --hash=sha256:f0c8fe5aeadd8a0e5b0598f8a6ee3533135ca50fd3f20f130f9d72baf5c6ac58 \ + --hash=sha256:f2cb63c49bc96847c3bb8dca350970e4dcd19936f391cfdfd057dcb37c4fa97e \ + --hash=sha256:f4d4efb218264ecf0f8516196c9e2d1a0679d9fb3bb15df1155a35220062eba8 \ + --hash=sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3 \ + --hash=sha256:f83c971eb9d2358b6a18da51dc0ae00556ac7c73104dde16e9e14c15aaf685ca \ + --hash=sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439 \ + --hash=sha256:ff1fb2dfef035549565a4124998fadcb7a3d4957131ddf004a56edeb029626b3 + # via mypy license-expression==30.4.4 \ --hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \ --hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd @@ -513,45 +597,45 @@ msgpack==1.1.2 \ --hash=sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e \ --hash=sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162 # via cachecontrol -mypy==1.18.2 \ - --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ - --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ - --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ - --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ - --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ - --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ - --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ - --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ - --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ - --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ - --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ - --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ - --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ - --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ - --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ - --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ - --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ - --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ - --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ - --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ - --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ - --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ - --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ - --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ - --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ - --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ - --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ - --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ - --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ - --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ - --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ - --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ - --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ - --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ - --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ - --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ - --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ - --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 +mypy==1.19.1 \ + --hash=sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd \ + --hash=sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b \ + --hash=sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1 \ + --hash=sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba \ + --hash=sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b \ + --hash=sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045 \ + --hash=sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac \ + --hash=sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6 \ + --hash=sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a \ + --hash=sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24 \ + --hash=sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957 \ + --hash=sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042 \ + --hash=sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e \ + --hash=sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec \ + --hash=sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3 \ + --hash=sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718 \ + --hash=sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f \ + --hash=sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331 \ + --hash=sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1 \ + --hash=sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1 \ + --hash=sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13 \ + --hash=sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67 \ + --hash=sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2 \ + --hash=sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a \ + --hash=sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b \ + --hash=sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8 \ + --hash=sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376 \ + --hash=sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef \ + --hash=sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288 \ + --hash=sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75 \ + --hash=sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74 \ + --hash=sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250 \ + --hash=sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab \ + --hash=sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6 \ + --hash=sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247 \ + --hash=sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925 \ + --hash=sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e \ + --hash=sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e # via python-basic-template (pyproject.toml) mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ @@ -563,17 +647,17 @@ nbformat==5.10.4 \ --hash=sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a \ --hash=sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b # via nbstripout -nbstripout==0.8.1 \ - --hash=sha256:79a8c8da488d98c54c112fa87185045f0271a97d84f1d46918d6a3ee561b30e7 \ - --hash=sha256:eaac8b6b4e729e8dfe1e5df2c0f8ba44abc5a17a65448f0480141f80be230bb1 +nbstripout==0.8.2 \ + --hash=sha256:2876530eb684bf93a5b48fe6d92b2163f78d040721c76b37d5b9e1514d38fc69 \ + --hash=sha256:5f06f9138cb64daed3e91c5359ff0fff85bab4d0db7d72723be1da6f51b890ae # via python-basic-template (pyproject.toml) -nodeenv==1.9.1 \ - --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ - --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 +nodeenv==1.10.0 \ + --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ + --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb # via pre-commit -packageurl-python==0.17.5 \ - --hash=sha256:a7be3f3ba70d705f738ace9bf6124f31920245a49fa69d4b416da7037dd2de61 \ - --hash=sha256:f0e55452ab37b5c192c443de1458e3f3b4d8ac27f747df6e8c48adeab081d321 +packageurl-python==0.17.6 \ + --hash=sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25 \ + --hash=sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9 # via cyclonedx-python-lib packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ @@ -584,9 +668,9 @@ packaging==25.0 \ # pip-audit # pip-requirements-parser # pytest -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 +pathspec==1.0.3 \ + --hash=sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d \ + --hash=sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c # via # black # mypy @@ -594,9 +678,9 @@ pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ --hash=sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625 # via pip-audit -pip-audit==2.9.0 \ - --hash=sha256:0b998410b58339d7a231e5aa004326a294e4c7c6295289cdc9d5e1ef07b1f44d \ - --hash=sha256:348b16e60895749a0839875d7cc27ebd692e1584ebe5d5cb145941c8e25a80bd +pip-audit==2.10.0 \ + --hash=sha256:16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f \ + --hash=sha256:427ea5bf61d1d06b98b1ae29b7feacc00288a2eced52c9c58ceed5253ef6c2a4 # via python-basic-template (pyproject.toml) pip-requirements-parser==32.0.1 \ --hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \ @@ -606,9 +690,9 @@ pip-tools==7.5.0 \ --hash=sha256:30639f50961bb09f49d22f4389e8d7d990709677c094ce1114186b1f2e9b5821 \ --hash=sha256:69758e4e5a65f160e315d74db46246fdbb30d549f1ed0c4236d057122c9b0f18 # via python-basic-template (pyproject.toml) -platformdirs==4.5.0 \ - --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ - --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 +platformdirs==4.5.1 \ + --hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \ + --hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31 # via # black # jupyter-core @@ -620,9 +704,9 @@ pluggy==1.6.0 \ # via # pytest # pytest-cov -pre-commit==4.3.0 \ - --hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \ - --hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16 +pre-commit==4.5.1 \ + --hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \ + --hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61 # via python-basic-template (pyproject.toml) py-serializable==2.1.0 \ --hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \ @@ -634,9 +718,9 @@ pygments==2.19.2 \ # via # pytest # rich -pyparsing==3.2.5 \ - --hash=sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6 \ - --hash=sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e +pyparsing==3.3.1 \ + --hash=sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82 \ + --hash=sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c # via pip-requirements-parser pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ @@ -644,9 +728,9 @@ pyproject-hooks==1.2.0 \ # via # build # pip-tools -pytest==8.4.2 \ - --hash=sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01 \ - --hash=sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79 +pytest==9.0.2 \ + --hash=sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b \ + --hash=sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11 # via # pytest-beartype # pytest-cov @@ -659,9 +743,9 @@ pytest-cov==7.0.0 \ --hash=sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1 \ --hash=sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861 # via python-basic-template (pyproject.toml) -pytokens==0.2.0 \ - --hash=sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43 \ - --hash=sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8 +pytokens==0.3.0 \ + --hash=sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a \ + --hash=sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3 # via black pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ @@ -760,193 +844,202 @@ rich==14.2.0 \ # via # pip-audit # python-basic-template (pyproject.toml) -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 +rpds-py==0.30.0 \ + --hash=sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f \ + --hash=sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136 \ + --hash=sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3 \ + --hash=sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7 \ + --hash=sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65 \ + --hash=sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4 \ + --hash=sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169 \ + --hash=sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf \ + --hash=sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4 \ + --hash=sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2 \ + --hash=sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c \ + --hash=sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4 \ + --hash=sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3 \ + --hash=sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6 \ + --hash=sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7 \ + --hash=sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89 \ + --hash=sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85 \ + --hash=sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6 \ + --hash=sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa \ + --hash=sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb \ + --hash=sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6 \ + --hash=sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87 \ + --hash=sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856 \ + --hash=sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4 \ + --hash=sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f \ + --hash=sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53 \ + --hash=sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229 \ + --hash=sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad \ + --hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \ + --hash=sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db \ + --hash=sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038 \ + --hash=sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27 \ + --hash=sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00 \ + --hash=sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18 \ + --hash=sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083 \ + --hash=sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c \ + --hash=sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738 \ + --hash=sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898 \ + --hash=sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e \ + --hash=sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7 \ + --hash=sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08 \ + --hash=sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6 \ + --hash=sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551 \ + --hash=sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e \ + --hash=sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288 \ + --hash=sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df \ + --hash=sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0 \ + --hash=sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2 \ + --hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \ + --hash=sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0 \ + --hash=sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464 \ + --hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \ + --hash=sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404 \ + --hash=sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7 \ + --hash=sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139 \ + --hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \ + --hash=sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb \ + --hash=sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15 \ + --hash=sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff \ + --hash=sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed \ + --hash=sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6 \ + --hash=sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e \ + --hash=sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95 \ + --hash=sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d \ + --hash=sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950 \ + --hash=sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3 \ + --hash=sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5 \ + --hash=sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97 \ + --hash=sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e \ + --hash=sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e \ + --hash=sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b \ + --hash=sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd \ + --hash=sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad \ + --hash=sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8 \ + --hash=sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425 \ + --hash=sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221 \ + --hash=sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d \ + --hash=sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825 \ + --hash=sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51 \ + --hash=sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e \ + --hash=sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f \ + --hash=sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8 \ + --hash=sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f \ + --hash=sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d \ + --hash=sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07 \ + --hash=sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877 \ + --hash=sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31 \ + --hash=sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58 \ + --hash=sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94 \ + --hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \ + --hash=sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000 \ + --hash=sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1 \ + --hash=sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1 \ + --hash=sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7 \ + --hash=sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7 \ + --hash=sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40 \ + --hash=sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d \ + --hash=sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0 \ + --hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \ + --hash=sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f \ + --hash=sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a \ + --hash=sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7 \ + --hash=sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419 \ + --hash=sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8 \ + --hash=sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a \ + --hash=sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9 \ + --hash=sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be \ + --hash=sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed \ + --hash=sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a \ + --hash=sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d \ + --hash=sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324 \ + --hash=sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f \ + --hash=sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2 \ + --hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f \ + --hash=sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5 # via # jsonschema # referencing -ruff==0.14.1 \ - --hash=sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5 \ - --hash=sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b \ - --hash=sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020 \ - --hash=sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69 \ - --hash=sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345 \ - --hash=sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1 \ - --hash=sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd \ - --hash=sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61 \ - --hash=sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf \ - --hash=sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192 \ - --hash=sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6 \ - --hash=sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151 \ - --hash=sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d \ - --hash=sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5 \ - --hash=sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c \ - --hash=sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6 \ - --hash=sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44 \ - --hash=sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896 \ - --hash=sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224 +ruff==0.14.11 \ + --hash=sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1 \ + --hash=sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841 \ + --hash=sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa \ + --hash=sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe \ + --hash=sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9 \ + --hash=sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0 \ + --hash=sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f \ + --hash=sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6 \ + --hash=sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984 \ + --hash=sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491 \ + --hash=sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3 \ + --hash=sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe \ + --hash=sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7 \ + --hash=sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf \ + --hash=sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2 \ + --hash=sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e \ + --hash=sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958 \ + --hash=sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e \ + --hash=sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6 # via python-basic-template (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via cyclonedx-python-lib -toml==0.10.2 \ - --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ - --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f +tomli==2.4.0 \ + --hash=sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729 \ + --hash=sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b \ + --hash=sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d \ + --hash=sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df \ + --hash=sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576 \ + --hash=sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d \ + --hash=sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1 \ + --hash=sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a \ + --hash=sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e \ + --hash=sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc \ + --hash=sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702 \ + --hash=sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6 \ + --hash=sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd \ + --hash=sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4 \ + --hash=sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776 \ + --hash=sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a \ + --hash=sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66 \ + --hash=sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87 \ + --hash=sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2 \ + --hash=sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f \ + --hash=sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475 \ + --hash=sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f \ + --hash=sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95 \ + --hash=sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9 \ + --hash=sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3 \ + --hash=sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9 \ + --hash=sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76 \ + --hash=sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da \ + --hash=sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8 \ + --hash=sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51 \ + --hash=sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86 \ + --hash=sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8 \ + --hash=sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0 \ + --hash=sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b \ + --hash=sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1 \ + --hash=sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e \ + --hash=sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d \ + --hash=sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c \ + --hash=sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867 \ + --hash=sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a \ + --hash=sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c \ + --hash=sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0 \ + --hash=sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4 \ + --hash=sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614 \ + --hash=sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132 \ + --hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \ + --hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087 + # via pip-audit +tomli-w==1.2.0 \ + --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ + --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 # via pip-audit traitlets==5.14.3 \ --hash=sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7 \ @@ -958,20 +1051,25 @@ typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via + # cyclonedx-python-lib # mypy # referencing -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc - # via requests -virtualenv==20.35.3 \ - --hash=sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44 \ - --hash=sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + # via + # python-basic-template (pyproject.toml) + # requests +virtualenv==20.36.1 \ + --hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \ + --hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba # via pre-commit -werkzeug==3.1.3 \ - --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ - --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 - # via flask +werkzeug==3.1.5 \ + --hash=sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc \ + --hash=sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67 + # via + # flask + # python-basic-template (pyproject.toml) wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 diff --git a/requirements-slim.txt b/requirements-slim.txt deleted file mode 100644 index 750b23de..00000000 --- a/requirements-slim.txt +++ /dev/null @@ -1,254 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile --cert=None --client-cert=None --generate-hashes --index-url=None --output-file=requirements-slim.txt --pip-args=None --strip-extras pyproject.toml -# -blinker==1.9.0 \ - --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ - --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc - # via flask -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 - # via requests -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 - # via requests -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 - # via flask -flask==2.3.3 \ - --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ - --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b - # via python-basic-template (pyproject.toml) -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 - # via requests -itsdangerous==2.2.0 \ - --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ - --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 - # via flask -jinja2==3.1.6 \ - --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ - --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via flask -markupsafe==3.0.3 \ - --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ - --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ - --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ - --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ - --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ - --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ - --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ - --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ - --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ - --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ - --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ - --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ - --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ - --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ - --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ - --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ - --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ - --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ - --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ - --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ - --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ - --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ - --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ - --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ - --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ - --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ - --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ - --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ - --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ - --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ - --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ - --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ - --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ - --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ - --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ - --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ - --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ - --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ - --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ - --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ - --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ - --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ - --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ - --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ - --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ - --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ - --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ - --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ - --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ - --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ - --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ - --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ - --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ - --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ - --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ - --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ - --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ - --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ - --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ - --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ - --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ - --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ - --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ - --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ - --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ - --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ - --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ - --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ - --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ - --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ - --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ - --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ - --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ - --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ - --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ - --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ - --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ - --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ - --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ - --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ - --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ - --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ - --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ - --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ - --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ - --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ - --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ - --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ - --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 - # via - # jinja2 - # werkzeug -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via python-basic-template (pyproject.toml) -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc - # via requests -werkzeug==3.1.3 \ - --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ - --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 - # via flask diff --git a/requirements.txt b/requirements.txt index 0d13a41f..d8c02b0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,15 +2,15 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --cert=None --client-cert=None --extra=extra --generate-hashes --index-url=None --output-file=requirements.txt --pip-args=None --strip-extras pyproject.toml +# pip-compile --cert=None --client-cert=None --generate-hashes --index-url=None --output-file=requirements.txt --pip-args=None --strip-extras pyproject.toml # blinker==1.9.0 \ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc # via flask -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +certifi==2026.1.4 \ + --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ + --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 # via requests charset-normalizer==3.4.4 \ --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ @@ -127,10 +127,14 @@ charset-normalizer==3.4.4 \ --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 # via requests -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.3.1 \ + --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ + --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 # via flask +filelock==3.20.3 \ + --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ + --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 + # via python-basic-template (pyproject.toml) flask==2.3.3 \ --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b @@ -244,11 +248,15 @@ requests==2.32.5 \ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf # via python-basic-template (pyproject.toml) -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc - # via requests -werkzeug==3.1.3 \ - --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ - --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 - # via flask +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + # via + # python-basic-template (pyproject.toml) + # requests +werkzeug==3.1.5 \ + --hash=sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc \ + --hash=sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67 + # via + # flask + # python-basic-template (pyproject.toml) diff --git a/scripts/dependency.ps1 b/scripts/dependency.ps1 deleted file mode 100644 index 82ce368f..00000000 --- a/scripts/dependency.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -# scripts/dependency.ps1 -# Usage: -# .\scripts\dependency.ps1 # install prod + dev -# .\scripts\dependency.ps1 --prod # only production -# .\scripts\dependency.ps1 --dev # only development - -param( - [switch]$prod, - [switch]$dev -) - -$ErrorActionPreference = 'Stop' - -# read same env vars as bash script, with defaults -$VENV_DIR = if ($env:VENV_DIR) { $env:VENV_DIR } else { '.venv' } -$PYPROJECT = if ($env:PYPROJECT) { $env:PYPROJECT } else { 'pyproject.toml' } -$PROD_LOCK = if ($env:PROD_LOCK) { $env:PROD_LOCK } else { 'requirements.txt' } -$DEV_LOCK = if ($env:DEV_LOCK) { $env:DEV_LOCK } else { 'requirements-dev.txt' } - -function Step($m){ Write-Host $m -ForegroundColor Yellow } -function Ok($m){ Write-Host $m -ForegroundColor Green } -function Fail($m){ Write-Host $m -ForegroundColor Red; exit 1 } - -# decide what to install -$installProd = $false -$installDev = $false -if (-not $prod -and -not $dev) { $installProd = $true; $installDev = $true } -if ($prod) { $installProd = $true } -if ($dev) { $installDev = $true } - -# activate venv -Step "โณ Activating virtualenv at '$VENV_DIR'..." -$activate = Join-Path $VENV_DIR 'Scripts\Activate.ps1' -if (-not (Test-Path $activate)) { - Fail "Virtualenv not found at '$VENV_DIR'. Create one with: python -m venv $VENV_DIR" -} -. $activate -Ok "โœ… Virtualenv activated" - -# ensure pip-tools -Step "๐Ÿ” Ensuring pip-tools is installed..." -$pipTools = & pip show pip-tools 2>$null -if (-not $pipTools) { - Step "โžก๏ธ Installing pip-tools..." - pip install pip-tools | Out-Null -} else { - Ok "โœ… pip-tools is already installed" -} - -# production -if ($installProd) { - Step "Compiling production lockfile from '$PYPROJECT'..." - pip-compile $PYPROJECT --output-file $PROD_LOCK --generate-hashes - - Step "Installing production dependencies..." - pip install --require-hashes -r $PROD_LOCK -} - -# development -if ($installDev) { - Step "Compiling development lockfile from '$PYPROJECT' extras..." - pip-compile $PYPROJECT --extra dev --output-file $DEV_LOCK --generate-hashes - - Step "Installing development dependencies..." - pip install --require-hashes -r $DEV_LOCK -} - -# verify -Step "๐Ÿ”Ž Verifying installed packages..." -pip check | Out-Host - -Ok "๐ŸŽ‰ Setup complete!" -if ($installProd) { Ok " โ€ข Production deps from $PROD_LOCK" } -if ($installDev) { Ok " โ€ข Dev deps from $DEV_LOCK" } diff --git a/scripts/dependency.sh b/scripts/dependency.sh index 6e0a849f..51b4ebfe 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -77,7 +77,12 @@ if [ -f "${VENV_DIR}/bin/activate" ]; then source "${VENV_DIR}/bin/activate" success "Virtual environment activated." else - error "Virtualenv not found at '${VENV_DIR}'. Please run setup script first." + info "Creating virtual environment at '${VENV_DIR}'..." + python3 -m venv "${VENV_DIR}" + success "Virtual environment created." + # shellcheck disable=SC1090 + source "${VENV_DIR}/bin/activate" + success "Virtual environment activated." fi step "Ensuring pip-tools is installed..." @@ -93,34 +98,64 @@ fi # MAJOR CHANGE: The script is now split into a COMPILE phase and an INSTALL phase. # ============================================================================= +# Check if there are extra optional dependencies +HAS_EXTRA_DEPS=false +if python3 -c "import tomllib; d = tomllib.load(open('${PYPROJECT}', 'rb')); deps = d.get('project', {}).get('optional-dependencies', {}).get('extra', []); exit(0 if len(deps) > 0 else 1)" 2>/dev/null; then + HAS_EXTRA_DEPS=true +fi + # ----------------------------------------------------------------------------- # PHASE 1: COMPILE ALL REQUIRED LOCKFILES # ----------------------------------------------------------------------------- # This block runs first to ensure our lockfiles are valid before we try to # install from them. This prevents errors if a file was manually edited. +# +# Logic: +# - Always compile requirements.txt and requirements-dev.txt +# - If extra deps exist: requirements.txt includes extra, requirements-slim.txt is core only +# - If no extra deps: requirements.txt is core, no slim file is created step "PHASE 1: Compiling lockfiles from '${PYPROJECT}'..." -info "Compiling production lockfile: '${PROD_LOCK}'" -pip-compile "${PYPROJECT}" \ - --extra=extra \ - --output-file="${PROD_LOCK}" \ - --generate-hashes \ - --strip-extras +if [ "$HAS_EXTRA_DEPS" = true ]; then + info "Extra dependencies detected. Compiling production lockfile with extras: '${PROD_LOCK}'" + pip-compile "${PYPROJECT}" \ + --extra=extra \ + --output-file="${PROD_LOCK}" \ + --generate-hashes \ + --strip-extras +else + info "No extra dependencies. Compiling base production lockfile: '${PROD_LOCK}'" + pip-compile "${PYPROJECT}" \ + --output-file="${PROD_LOCK}" \ + --generate-hashes \ + --strip-extras +fi info "Compiling development lockfile: '${DEV_LOCK}'" -pip-compile "${PYPROJECT}" \ - --extra=dev \ - --extra=extra \ - --output-file="${DEV_LOCK}" \ - --generate-hashes \ - --strip-extras - -info "Compiling slim lockfile: '${SLIM_LOCK}'" -pip-compile "${PYPROJECT}" \ - --output-file="${SLIM_LOCK}" \ - --generate-hashes \ - --strip-extras +if [ "$HAS_EXTRA_DEPS" = true ]; then + pip-compile "${PYPROJECT}" \ + --extra=dev \ + --extra=extra \ + --output-file="${DEV_LOCK}" \ + --generate-hashes \ + --strip-extras +else + pip-compile "${PYPROJECT}" \ + --extra=dev \ + --output-file="${DEV_LOCK}" \ + --generate-hashes \ + --strip-extras +fi + +# Only compile slim if there are extra dependencies +if [ "$HAS_EXTRA_DEPS" = true ]; then + info "Compiling slim lockfile (core only): '${SLIM_LOCK}'" + pip-compile "${PYPROJECT}" \ + --output-file="${SLIM_LOCK}" \ + --generate-hashes \ + --strip-extras +fi success "All lockfiles compiled." @@ -167,3 +202,6 @@ else [ "$INSTALL_DEV" = true ] && echo " โ€ข Development dependencies installed from '${DEV_LOCK}'" [ "$INSTALL_SLIM" = true ] && echo " โ€ข Slim dependencies installed from '${SLIM_LOCK}'" fi +if [ "$HAS_EXTRA_DEPS" = true ]; then + echo " โ€ข Slim requirements file available: ${SLIM_LOCK}" +fi diff --git a/scripts/env.ps1 b/scripts/env.ps1 deleted file mode 100644 index d8a2d0a5..00000000 --- a/scripts/env.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -# scripts/env.ps1 -$envFile = Join-Path (Get-Location) ".env" -if (Test-Path $envFile) { - Get-Content $envFile | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith("#")) { - $kv = $line -split "=", 2 - if ($kv.Count -eq 2) { - $key = $kv[0].Trim() - $val = $kv[1].Trim().Trim("'`"").Replace("`r","") - $val = [Environment]::ExpandEnvironmentVariables($val) - Set-Item -Path Env:$key -Value $val - } - } - } -} - -if (-not $env:PYTHONPATH) { $env:PYTHONPATH = "." } -if (-not $env:VENV_DIR) { $env:VENV_DIR = ".venv" } -if (-not $env:PROD_LOCK) { $env:PROD_LOCK = "requirements.txt" } -if (-not $env:DEV_LOCK) { $env:DEV_LOCK = "requirements-dev.txt" } -if (-not $env:PYPROJECT) { $env:PYPROJECT = "pyproject.toml" } - -$activate = Join-Path $env:VENV_DIR "Scripts\Activate.ps1" -if (Test-Path $activate) { . $activate } diff --git a/scripts/hooks/nbstripout-install.ps1 b/scripts/hooks/nbstripout-install.ps1 deleted file mode 100644 index 193ab5d5..00000000 --- a/scripts/hooks/nbstripout-install.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -# Installs nbstripout as a Git filter for the current repo -# Run once per repo, or call from setup.ps1 - -$ErrorActionPreference = "Stop" - -# Ensure nbstripout is present in the venv -if (-not (python -m pip show nbstripout 2>$null)) { - python -m pip install nbstripout | Out-Null -} - -# Install the filter in this repo -python -m nbstripout --install - -Write-Host "โœ… nbstripout Git filter installed for this repository." -ForegroundColor Green diff --git a/scripts/hooks/precommit_pytest.ps1 b/scripts/hooks/precommit_pytest.ps1 deleted file mode 100644 index c106be24..00000000 --- a/scripts/hooks/precommit_pytest.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -$env:PYTHONDONTWRITEBYTECODE = "1" -python -m pytest --cov --cov-fail-under=80 --beartype-packages=utils,example -exit $LASTEXITCODE diff --git a/scripts/hooks/verify-dev-lockfile.ps1 b/scripts/hooks/verify-dev-lockfile.ps1 deleted file mode 100644 index fc745135..00000000 --- a/scripts/hooks/verify-dev-lockfile.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -# Wrapper for requirements-dev.txt -$ErrorActionPreference = "Stop" -& "$PSScriptRoot\verify-lockfile.ps1" -Lockfile "requirements-dev.txt" diff --git a/scripts/hooks/verify-lockfile.ps1 b/scripts/hooks/verify-lockfile.ps1 deleted file mode 100644 index 52fc7e54..00000000 --- a/scripts/hooks/verify-lockfile.ps1 +++ /dev/null @@ -1,43 +0,0 @@ -Param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$Lockfile, - - [string]$PyProject = "pyproject.toml" -) - -$ErrorActionPreference = "Stop" - -function Step($m){ Write-Host "๐Ÿ‘‰ $m" -ForegroundColor Cyan } -function Ok($m){ Write-Host "โœ… $m" -ForegroundColor Green } -function Fail($m){ Write-Host "โŒ $m" -ForegroundColor Red; exit 1 } - -if (-not (Test-Path $PyProject)) { Fail "Cannot find $PyProject in repo root" } -if (-not (Test-Path $Lockfile)) { Fail "Cannot find $Lockfile in repo root" } - -$python = (Get-Command python -ErrorAction SilentlyContinue) -if (-not $python) { Fail "Python not found, install Python 3.12+ and ensure it is on PATH" } - -$temp = [System.IO.Path]::GetTempFileName() -try { - Step "Verifying integrity of '$Lockfile'" - - $isDev = ($Lockfile -like "*requirements-dev.txt") - - $args = @("piptools", "compile", $PyProject, "--output-file", $temp) - if ($isDev) { $args += @("--extra", "dev") } - - python -m $args | Out-Null - - $hashA = Get-FileHash -Algorithm SHA256 -Path $Lockfile - $hashB = Get-FileHash -Algorithm SHA256 -Path $temp - - if ($hashA.Hash -ne $hashB.Hash) { - Fail ("'$Lockfile' is out of date. Remove it and run '.\scripts\dependency.ps1', then commit the regenerated file.") - } - - Ok "'$Lockfile' is valid and up to date" -} -finally { - if (Test-Path $temp) { Remove-Item $temp -Force -ErrorAction SilentlyContinue } -} diff --git a/scripts/hooks/verify-prod-lockfile.ps1 b/scripts/hooks/verify-prod-lockfile.ps1 deleted file mode 100644 index ba48b932..00000000 --- a/scripts/hooks/verify-prod-lockfile.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -# Wrapper for requirements.txt -$ErrorActionPreference = "Stop" -& "$PSScriptRoot\verify-lockfile.ps1" -Lockfile "requirements.txt" diff --git a/scripts/new-branch.ps1 b/scripts/new-branch.ps1 deleted file mode 100644 index 209a7247..00000000 --- a/scripts/new-branch.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -<# -Creates a new branch from origin/main and sets upstream. -Usage: - .\scripts\new-branch.ps1 feature/my-new-thing -#> - -param( - [Parameter(Position=0, Mandatory=$false)] - [string]$Name -) - -$ErrorActionPreference = "Stop" - -function Fail($m){ Write-Host "โŒ $m" -ForegroundColor Red; exit 1 } -function Step($m){ Write-Host "๐Ÿ‘‰ $m" -ForegroundColor Cyan } -function Ok($m){ Write-Host "โœ… $m" -ForegroundColor Green } - -if (-not $Name -or $Name.Trim() -eq "") { - $Name = Read-Host "Enter new branch name (e.g. feature/my-new-thing)" - if (-not $Name) { Fail "Branch name is required." } -} - -# Basic validation -if ($Name -match '^\s|[\s]$') { Fail "Branch name cannot start or end with space." } -if ($Name -match '[~^:\?\*\[\]\\ ]') { Fail "Branch name contains invalid characters." } - -# Ensure we are in a Git repo -git rev-parse --git-dir *> $null 2>&1 || Fail "Not inside a Git repository." - -Step "Fetching latest refs" -git fetch --prune - -# Ensure main exists locally -$main = "main" -if (-not (git show-ref --verify --quiet "refs/heads/$main")) { - Step "Creating local '$main' tracking branch" - git checkout -B $main "origin/$main" -} else { - Step "Checking out '$main'" - git checkout $main -} - -Step "Resetting '$main' to origin/$main" -git reset --hard "origin/$main" - -Step "Creating new branch '$Name' from origin/$main" -git checkout -B $Name "origin/$main" - -Step "Pushing and setting upstream" -git push -u origin $Name - -Ok "Branch '$Name' ready and tracking origin/$Name." diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 deleted file mode 100644 index 10086a0a..00000000 --- a/scripts/setup.ps1 +++ /dev/null @@ -1,75 +0,0 @@ -# scripts/setup.ps1 -<# Complete Windows setup for Python Basic Template #> -Param( - [switch]$DevOnly, - [switch]$ProdOnly -) - -$ErrorActionPreference = "Stop" - -function Step($m){ Write-Host "๐Ÿ‘‰ $m" -ForegroundColor Cyan } -function Ok($m){ Write-Host "โœ… $m" -ForegroundColor Green } -function Warn($m){ Write-Host "โš  $m" -ForegroundColor Yellow } -function Fail($m){ Write-Host "โŒ $m" -ForegroundColor Red; exit 1 } - -# Load env (similar to .envrc) -. (Join-Path (Get-Location) "scripts\env.ps1") - -# 1. sanity checks -Step "Checking prerequisites" -$py = (Get-Command python -ErrorAction SilentlyContinue) -if(-not $py){ Fail "Python not found, install Python 3.12+ from python.org or winget" } -$git = (Get-Command git -ErrorAction SilentlyContinue) -if(-not $git){ Fail "git not found, install Git for Windows" } -Ok "Python: $(python --version)" -Ok "git: $(git --version)" - -# 2. venv -Step "Creating and activating virtual environment" -$venv = if ($env:VENV_DIR) { $env:VENV_DIR } else { "venv" } -if(Test-Path $venv){ Warn "Existing venv found, reusing" } else { python -m venv $venv } -$activate = Join-Path $venv "Scripts\Activate.ps1" -. $activate -Ok "Virtual environment activated" - -# 3. pip tooling -Step "Upgrading pip and wheel" -python -m pip install --upgrade pip wheel build pip-tools - -# 4. dependencies (calls dependency.ps1) -if($DevOnly){ .\scripts\dependency.ps1 --dev } -elseif($ProdOnly){ .\scripts\dependency.ps1 --prod } -else { .\scripts\dependency.ps1 } - -# 5. developer tools -Step "Installing developer tools" -pip install pre-commit detect-secrets nbstripout pip-audit jupytext pytest pytest-cov pytest-beartype beartype -try { python -m piptools --version | Out-Null } catch { Warn "pip-tools not on PATH yet; pre-commit hooks may fail until a new shell is opened" } - -# 6. Installing project -Step "Installing project in editable mode" -pip install -e . - -# 7. pre-commit hooks -Step "Installing pre-commit hooks" -if (Test-Path ".pre-commit-windows.yaml") { - pre-commit install -c .pre-commit-windows.yaml - pre-commit autoupdate -c .pre-commit-windows.yaml - try { pre-commit run --all-files -c .pre-commit-windows.yaml } catch { Warn "Pre-commit found issues, fix then re-run" } -} else { - Warn ".pre-commit-windows.yaml not found, skipping pre-commit setup" -} - -# 8. Install nbstripout -try { - .\scripts\hooks\nbstripout-install.ps1 -} catch { - Warn "nbstripout install skipped: $($_.Exception.Message)" -} - -# 9. validation -Step "Running import smoke test" -$env:PYTHONPATH = (Get-Location).Path -python -c "import utils.secrets; print('โœ… All project modules import successfully')" - -Ok "๐ŸŽ‰ Windows setup complete" From b26a567678d727ed08012eea7af3c50bbed4ec2e Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 14 Jan 2026 10:28:25 +0100 Subject: [PATCH 089/199] Refactor code structure for improved readability and maintainability --- README.md | 237 +++++--- pyproject.toml | 1 - requirements-dev.txt | 1082 ----------------------------------- requirements.txt | 262 --------- scripts/dependency.sh | 332 +++++------ uv.lock | 1257 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1579 insertions(+), 1592 deletions(-) delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt create mode 100644 uv.lock diff --git a/README.md b/README.md index b2c3ee72..5df27fdb 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,16 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co ### Dependencies * **Do not** run `pip install` manually in the terminal! - Why: it creates unpinned installs that differ per user and will break CI -* Always declare packages in pyproject.toml only -* Run the helper script to handle the package install for you -- All dependencies (dev + extra + core): `./scripts/dependency.sh` or `./scripts/dependency.sh --dev` - - Install this locally for developement -- Production dependencies (core + extra): `./scripts/dependency.sh --prod` - - Install this in staging or production environments. It contains all needed to run the project without dev tools. -- Core dependencies only (minimal): `./scripts/dependency.sh --slim` - - Use this in CI pipelines. It contains the core dependencies to run the project, nothing more. - -* After you change pyproject.toml run the script again. This will update all requirement files automatically. -* If you remove dependencies from pyproject.toml that are no longeer needed, delete your virtualenvironment and run the script. + Why: it bypasses the lock file and creates inconsistencies across environments +* Always declare packages in `pyproject.toml` only +* Use `uv sync` to install dependencies from the lock file +* Common commands: + - First time setup: `./scripts/dependency.sh` + - Install deps: `uv sync` + - Add a package: Edit `pyproject.toml`, then run `uv lock && uv sync` + - Check for updates: `./scripts/dependency.sh --check` + - Update packages: `./scripts/dependency.sh --update` +* The lock file (`uv.lock`) ensures everyone uses the same versions ### Coding * Prefer small functions and clear modules so code is easy to test and reuse @@ -61,6 +59,7 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co - curl - sudo access (for system package installation) - 1Password CLI (see [docs/0_setup.md](docs/0_setup.md) for setup instructions) +- **uv** (see [Installing uv](#installing-uv) below if you don't have it yet) ### Linux: One-Command Setup @@ -80,6 +79,26 @@ cd {REPO_NAME} This will set up everything you need for development! +### Installing uv + +If you don't have `uv` installed yet, follow these instructions for your platform: + +#### Linux / macOS + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +Then restart your terminal. + +#### Verify Installation + +```bash +uv --version +``` + +You should see something like: `uv 0.x.x` + ### Common setup issues #### Issue: Windows setup Powershell Execution Policy @@ -107,9 +126,10 @@ This only changes the policy for your user account. It allows you to run local s ## ๐Ÿ“‹ Features ### ๐Ÿ”ง **Modern Dependency Management** -- **Hybrid approach**: Production dependencies via `pip-tools`, development tools via `pyproject.toml` -- **Reproducible builds** with pinned versions -- **Clean separation** between production and development dependencies +- **uv-powered**: Fast, reliable dependency resolution and installation +- **Reproducible builds** with `uv.lock` lockfile +- **Single source of truth** in `pyproject.toml` +- **Simple, clear workflow** - no hash verification hassles ### ๐Ÿ” **Secret Management** - **1Password CLI integration** for secure secret handling @@ -132,6 +152,7 @@ python_basic_template/ โ”‚ โ””โ”€โ”€ workflows/ # GitHub Actions workflows โ”œโ”€โ”€ docs/ # Documentation (setup, secrets, dependencies, quality, progress) โ”‚ โ”œโ”€โ”€ 0_setup.md +โ”‚ โ”œโ”€โ”€ 1_usage.md โ”‚ โ”œโ”€โ”€ 1_secret_management.md โ”‚ โ”œโ”€โ”€ 2_dependency_management.md โ”‚ โ”œโ”€โ”€ 3_pre_commit_hooks.md @@ -143,117 +164,169 @@ python_basic_template/ โ”‚ โ””โ”€โ”€ using_secrets.ipynb โ”œโ”€โ”€ scripts/ # Setup and utility scripts โ”‚ โ”œโ”€โ”€ setup.sh -โ”‚ โ”œโ”€โ”€ dependency.sh +โ”‚ โ”œโ”€โ”€ dependency.sh # uv-based dependency management +โ”‚ โ”œโ”€โ”€ update_deps.py # Update package versions โ”‚ โ”œโ”€โ”€ new-branch.sh โ”‚ โ””โ”€โ”€ hooks/ โ”‚ โ”œโ”€โ”€ black-autoformat.sh -โ”‚ โ”œโ”€โ”€ check-requirements.sh -โ”‚ โ””โ”€โ”€ nbstripout-autoadd.sh +โ”‚ โ”œโ”€โ”€ nbstripout-autoadd.sh +โ”‚ โ””โ”€โ”€ precommit_pytest.sh โ”œโ”€โ”€ utils/ # Utility modules โ”‚ โ”œโ”€โ”€ __init__.py โ”‚ โ”œโ”€โ”€ secrets.py โ”‚ โ””โ”€โ”€ utils.py -โ”œโ”€โ”€ tests/ # (Empty) Test directory scaffold -โ”œโ”€โ”€ pyproject.toml # Project configuration & all dependencies -โ”œโ”€โ”€ requirements-slim.txt # Compiled core dependencies only -โ”œโ”€โ”€ requirements.txt # Compiled production dependencies (core + extra) -โ”œโ”€โ”€ requirements-dev.txt # Compiled all dependencies (core + extra + dev) +โ”œโ”€โ”€ tests/ # Test directory +โ”œโ”€โ”€ pyproject.toml # Project config & dependencies (single source of truth) +โ”œโ”€โ”€ uv.lock # Locked dependency versions (auto-generated) โ”œโ”€โ”€ .pre-commit-config.yaml # Pre-commit hooks config โ”œโ”€โ”€ .envrc # direnv environment config โ”œโ”€โ”€ .secrets.baseline # Secret detection baseline โ”œโ”€โ”€ CODE_OF_CONDUCT.md # Contributor code of conduct โ”œโ”€โ”€ CONTRIBUTING.md # Contribution guidelines -โ”œโ”€โ”€ setup.py # (Optional) Legacy setup script โ””โ”€โ”€ README.md # Project overview (this file) ``` -## ๐Ÿ—๏ธ Dependency Management +## ๐Ÿ—๏ธ Dependency Management with uv -This project uses a **layered dependency management approach** with three distinct groups: +This project uses **uv** for fast, reliable dependency management. uv provides a single-file lock format and unified workflow for all dependency tasks. -### Core Dependencies -- **Purpose**: Essential runtime requirements needed in all environments -- **Location**: `dependencies` section in `pyproject.toml` -- **Compiled to**: `requirements-slim.txt` -- **Examples**: Flask, requests, database drivers +### First Time Setup -### Extra Dependencies -- **Purpose**: Additional production features and optional components -- **Location**: `[project.optional-dependencies].extra` in `pyproject.toml` -- **Compiled to**: `requirements.txt` (core + extra) -- **Examples**: Private packages, optional integrations, performance libraries +```bash +# Step 1: Make sure uv is installed (see "Installing uv" section above) +uv --version -### Development Dependencies -- **Purpose**: Development tools, testing frameworks, and code quality tools -- **Location**: `[project.optional-dependencies].dev` in `pyproject.toml` -- **Compiled to**: `requirements-dev.txt` (core + extra + dev) -- **Examples**: pytest, black, mypy, pre-commit +# Step 2: Setup environment and lock dependencies +./scripts/dependency.sh +``` -This layered approach ensures: -- **Minimal deployments** with slim requirements for containers/lambdas -- **Full production** setup with all features enabled -- **Complete development** environment with all tooling -- **Reproducible builds** with fully pinned, hashed lockfiles +The `dependency.sh` script will: +1. โœ… Create a virtual environment (`.venv`) +2. โœ… Lock all dependencies into `uv.lock` +3. โœ… Sync and install everything -### Installation Options +**Important:** With uv, you don't need to activate the virtual environment! Instead: +- Use `uv run python script.py` to run Python scripts +- Use `uv run pytest` to run tests +- Use `uv run mypy .` to run type checking -| Command | Dependencies Installed | Use Case | -|---------|----------------------|----------| -| `./scripts/dependency.sh --slim` | Core only | Minimal deployments, containers, lambdas | -| `./scripts/dependency.sh --prod` | Core + Extra | Full production environment | -| `./scripts/dependency.sh --dev` | Development only | CI/testing environments | -| `./scripts/dependency.sh` | Core + Extra + Dev | Complete development setup (Default) | +(Optional) If you prefer traditional shell activation: +```bash +source .venv/bin/activate # Linux/macOS +# or: .venv\Scripts\activate # Windows PowerShell +``` -### Security and Hash Verification +### Quick Start -This project uses **hash-based verification** for maximum security against supply chain attacks: +```bash +# First time: setup environment and lock dependencies +./scripts/dependency.sh + +# Day-to-day: install locked dependencies +uv sync -- **Lockfiles include SHA256 hashes** for each dependency -- **Installation verifies packages** against these hashes -- **Prevents tampering** during the dependency supply chain +# Add a new package +# 1. Edit pyproject.toml and add the package +# 2. Run: uv lock && uv sync -#### When Hash Verification Fails +# Check for updates (without installing) +./scripts/dependency.sh --check -Some packages **cannot be hashed** due to: -- Private packages from git repositories -- Local development packages (`file://` URLs) -- Packages with dynamic content +# Update packages (interactive prompt) +./scripts/dependency.sh --update -**For these cases**, modify `scripts/dependency.sh`: -1. Remove `--require-hashes` from `pip install` commands -2. Remove `--generate-hashes` from corresponding `pip-compile` commands +# Update packages (auto-confirm all) +./scripts/dependency.sh --update -y +``` -**โš ๏ธ Security Warning:** Only disable hashes for packages that genuinely cannot be hashed. Keep verification enabled wherever possible. +### Dependency Layers -### Adding Dependencies +Dependencies are organized into three logical groups: -When adding new packages to your project, choose the appropriate section in `pyproject.toml`: +| Layer | Location | Purpose | Installed With | +|-------|----------|---------|-----------------| +| **Core** | `dependencies` | Essential runtime packages | `uv sync` | +| **Extra** | `[project.optional-dependencies].extra` | Optional production features | `uv sync` | +| **Dev** | `[project.optional-dependencies].dev` | Development and testing tools | `uv sync` (default) | + +**Example structure in `pyproject.toml`:** ```toml -# Core dependencies - Essential runtime requirements +# Core dependencies - Always installed dependencies = [ - "flask>=2.2.0,<3.0.0", # Web framework - "requests", # HTTP client - "sqlalchemy", # Database ORM + "flask>=2.2.0,<3.0.0", + "requests", + "sqlalchemy", ] -# Extra dependencies - Optional production features [project.optional-dependencies] +# Extra production features extra = [ - "redis", # Caching - "celery", # Task queue - "gunicorn", # Production server + "redis", + "celery", ] -# Dev dependencies - Development and testing tools +# Development tools dev = [ - "pytest>=7.0", # Testing framework - "black", # Code formatter - "mypy", # Type checker + "pytest>=7.0", + "black", + "mypy", + "pre-commit", ] ``` -**๐Ÿ“– Detailed guide**: [docs/2_dependency_management.md](docs/2_dependency_management.md) +### Common Workflows + +**Install all dependencies (development setup):** +```bash +uv sync +``` + +**Install production dependencies only:** +```bash +uv sync --no-dev +``` + +**Check for outdated packages:** +```bash +./scripts/dependency.sh --check +``` + +**Update packages (interactive - asks for confirmation):** +```bash +./scripts/dependency.sh --update +``` + +**Update packages automatically (no prompts):** +```bash +./scripts/dependency.sh --update -y +``` + +**Update a specific package:** +```bash +# Edit pyproject.toml to the version you want, then: +uv lock +uv sync +``` + +### Lock File + +The `uv.lock` file contains: +- All direct and transitive dependencies +- Pinned versions ensuring reproducibility +- Python version compatibility info + +**Always commit `uv.lock` to version control** to ensure consistent environments across team members and CI/CD. + +### Benefits Over pip-tools + +- โšก **Much faster** dependency resolution +- ๐Ÿ“ **Simpler lock format** - one file, human-readable +- ๐Ÿ”„ **Unified workflow** - one tool for all dependency tasks +- ๐Ÿ›ก๏ธ **Better error messages** - clearer dependency conflict reporting +- ๐Ÿš€ **Modern Python packaging** - supports PEP 508, PEP 517, PEP 660 + +**๐Ÿ“– Detailed guide**: [docs/3_dependency_management.md](docs/3_dependency_management.md) ## ๐Ÿ” Secret Management diff --git a/pyproject.toml b/pyproject.toml index 40c4d7ff..f8c7a2ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,6 @@ dev = [ "beartype", "pytest-beartype", "pip-audit>=2.9.0", - "pip-tools==7.5.0", # for managing requirement*.txt "rich", ] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index db5b9d50..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,1082 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile --cert=None --client-cert=None --extra=dev --generate-hashes --index-url=None --output-file=requirements-dev.txt --pip-args=None --strip-extras pyproject.toml -# -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 - # via - # jsonschema - # referencing -beartype==0.22.9 \ - --hash=sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f \ - --hash=sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2 - # via - # pytest-beartype - # python-basic-template (pyproject.toml) -black==25.12.0 \ - --hash=sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783 \ - --hash=sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f \ - --hash=sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5 \ - --hash=sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf \ - --hash=sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43 \ - --hash=sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea \ - --hash=sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be \ - --hash=sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f \ - --hash=sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d \ - --hash=sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892 \ - --hash=sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a \ - --hash=sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828 \ - --hash=sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655 \ - --hash=sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a \ - --hash=sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b \ - --hash=sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7 \ - --hash=sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f \ - --hash=sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83 \ - --hash=sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f \ - --hash=sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5 \ - --hash=sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5 \ - --hash=sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da \ - --hash=sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce \ - --hash=sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59 \ - --hash=sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a \ - --hash=sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b \ - --hash=sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8 - # via python-basic-template (pyproject.toml) -blinker==1.9.0 \ - --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ - --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc - # via flask -boolean-py==5.0 \ - --hash=sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95 \ - --hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9 - # via license-expression -build==1.4.0 \ - --hash=sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596 \ - --hash=sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936 - # via pip-tools -cachecontrol==0.14.4 \ - --hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b \ - --hash=sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1 - # via - # cachecontrol - # pip-audit -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 - # via requests -cfgv==3.5.0 \ - --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ - --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 - # via pre-commit -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 - # via requests -click==8.3.1 \ - --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ - --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 - # via - # black - # flask - # pip-tools -coverage==7.13.1 \ - --hash=sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784 \ - --hash=sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e \ - --hash=sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3 \ - --hash=sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0 \ - --hash=sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79 \ - --hash=sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75 \ - --hash=sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6 \ - --hash=sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f \ - --hash=sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88 \ - --hash=sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b \ - --hash=sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573 \ - --hash=sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0 \ - --hash=sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2 \ - --hash=sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0 \ - --hash=sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba \ - --hash=sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd \ - --hash=sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c \ - --hash=sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673 \ - --hash=sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e \ - --hash=sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee \ - --hash=sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3 \ - --hash=sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e \ - --hash=sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461 \ - --hash=sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6 \ - --hash=sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6 \ - --hash=sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500 \ - --hash=sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a \ - --hash=sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9 \ - --hash=sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc \ - --hash=sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5 \ - --hash=sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3 \ - --hash=sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d \ - --hash=sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842 \ - --hash=sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1 \ - --hash=sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486 \ - --hash=sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29 \ - --hash=sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968 \ - --hash=sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9 \ - --hash=sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4 \ - --hash=sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c \ - --hash=sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a \ - --hash=sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465 \ - --hash=sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8 \ - --hash=sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09 \ - --hash=sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416 \ - --hash=sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894 \ - --hash=sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a \ - --hash=sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9 \ - --hash=sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4 \ - --hash=sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5 \ - --hash=sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f \ - --hash=sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb \ - --hash=sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd \ - --hash=sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae \ - --hash=sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a \ - --hash=sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e \ - --hash=sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564 \ - --hash=sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd \ - --hash=sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6 \ - --hash=sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f \ - --hash=sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7 \ - --hash=sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a \ - --hash=sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee \ - --hash=sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62 \ - --hash=sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d \ - --hash=sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef \ - --hash=sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b \ - --hash=sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1 \ - --hash=sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78 \ - --hash=sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398 \ - --hash=sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53 \ - --hash=sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d \ - --hash=sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c \ - --hash=sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909 \ - --hash=sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f \ - --hash=sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9 \ - --hash=sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90 \ - --hash=sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851 \ - --hash=sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147 \ - --hash=sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf \ - --hash=sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864 \ - --hash=sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4 \ - --hash=sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf \ - --hash=sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4 \ - --hash=sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a \ - --hash=sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4 \ - --hash=sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c \ - --hash=sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992 \ - --hash=sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c \ - --hash=sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19 \ - --hash=sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7 \ - --hash=sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766 - # via pytest-cov -cyclonedx-python-lib==11.6.0 \ - --hash=sha256:7fb85a4371fa3a203e5be577ac22b7e9a7157f8b0058b7448731474d6dea7bf0 \ - --hash=sha256:94f4aae97db42a452134dafdddcfab9745324198201c4777ed131e64c8380759 - # via pip-audit -defusedxml==0.7.1 \ - --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ - --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 - # via py-serializable -detect-secrets==1.5.0 \ - --hash=sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a \ - --hash=sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060 - # via python-basic-template (pyproject.toml) -distlib==0.4.0 \ - --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ - --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d - # via virtualenv -fastjsonschema==2.21.2 \ - --hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \ - --hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de - # via nbformat -filelock==3.20.3 \ - --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ - --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 - # via - # cachecontrol - # python-basic-template (pyproject.toml) - # virtualenv -flask==2.3.3 \ - --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ - --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b - # via python-basic-template (pyproject.toml) -identify==2.6.16 \ - --hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \ - --hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980 - # via pre-commit -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 - # via requests -iniconfig==2.3.0 \ - --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ - --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 - # via pytest -itsdangerous==2.2.0 \ - --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ - --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 - # via flask -jinja2==3.1.6 \ - --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ - --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via flask -jsonschema==4.26.0 \ - --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ - --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce - # via nbformat -jsonschema-specifications==2025.9.1 \ - --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ - --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d - # via jsonschema -jupyter-core==5.9.1 \ - --hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \ - --hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 - # via nbformat -librt==0.7.7 \ - --hash=sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7 \ - --hash=sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832 \ - --hash=sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848 \ - --hash=sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc \ - --hash=sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91 \ - --hash=sha256:14ef0f4ac3728ffd85bfc58e2f2f48fb4ef4fa871876f13a73a7381d10a9f77c \ - --hash=sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630 \ - --hash=sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b \ - --hash=sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977 \ - --hash=sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8 \ - --hash=sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2 \ - --hash=sha256:264720fc288c86039c091a4ad63419a5d7cabbf1c1c9933336a957ed2483e570 \ - --hash=sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b \ - --hash=sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc \ - --hash=sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab \ - --hash=sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456 \ - --hash=sha256:44d63ce643f34a903f09ff7ca355aae019a3730c7afd6a3c037d569beeb5d151 \ - --hash=sha256:467dbd7443bda08338fc8ad701ed38cef48194017554f4c798b0a237904b3f99 \ - --hash=sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949 \ - --hash=sha256:4bf3cc46d553693382d2abf5f5bd493d71bb0f50a7c0beab18aa13a5545c8900 \ - --hash=sha256:4dcee2f921a8632636d1c37f1bbdb8841d15666d119aa61e5399c5268e7ce02e \ - --hash=sha256:50d1d1ee813d2d1a3baf2873634ba506b263032418d16287c92ec1cc9c1a00cb \ - --hash=sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d \ - --hash=sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111 \ - --hash=sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398 \ - --hash=sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259 \ - --hash=sha256:6ae8aec43117a645a31e5f60e9e3a0797492e747823b9bda6972d521b436b4e8 \ - --hash=sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d \ - --hash=sha256:7b16ccaeff0ed4355dfb76fe1ea7a5d6d03b5ad27f295f77ee0557bc20a72495 \ - --hash=sha256:7d13cc340b3b82134f8038a2bfe7137093693dcad8ba5773da18f95ad6b77a8a \ - --hash=sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a \ - --hash=sha256:8185c8497d45164e256376f9da5aed2bb26ff636c798c9dabe313b90e9f25b28 \ - --hash=sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c \ - --hash=sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b \ - --hash=sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6 \ - --hash=sha256:8f2f8dcf5ab9f80fb970c6fd780b398efb2f50c1962485eb8d3ab07788595a48 \ - --hash=sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244 \ - --hash=sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d \ - --hash=sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf \ - --hash=sha256:983de36b5a83fe9222f4f7dcd071f9b1ac6f3f17c0af0238dadfb8229588f890 \ - --hash=sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45 \ - --hash=sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811 \ - --hash=sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff \ - --hash=sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c \ - --hash=sha256:a1f5cc41a570269d1be7a676655875e3a53de4992a9fa38efb7983e97cf73d7c \ - --hash=sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba \ - --hash=sha256:a487b71fbf8a9edb72a8c7a456dda0184642d99cd007bc819c0b7ab93676a8ee \ - --hash=sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee \ - --hash=sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029 \ - --hash=sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d \ - --hash=sha256:ab2a2a9cd7d044e1a11ca64a86ad3361d318176924bbe5152fbc69f99be20b8c \ - --hash=sha256:ad3fc2d859a709baf9dd9607bb72f599b1cfb8a39eafd41307d0c3c4766763cb \ - --hash=sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1 \ - --hash=sha256:aea05f701ccd2a76b34f0daf47ca5068176ff553510b614770c90d76ac88df06 \ - --hash=sha256:b8bb331aad734b059c4b450cd0a225652f16889e286b2345af5e2c3c625c3d85 \ - --hash=sha256:bdb9f3d865b2dafe7f9ad7f30ef563c80d0ddd2fdc8cc9b8e4f242f475e34d75 \ - --hash=sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536 \ - --hash=sha256:c48c7e150c095d5e3cea7452347ba26094be905d6099d24f9319a8b475fcd3e0 \ - --hash=sha256:c7e5070cf3ec92d98f57574da0224f8c73faf1ddd6d8afa0b8c9f6e86997bc74 \ - --hash=sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd \ - --hash=sha256:c8ffe3431d98cc043a14e88b21288b5ec7ee12cb01260e94385887f285ef9389 \ - --hash=sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe \ - --hash=sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3 \ - --hash=sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804 \ - --hash=sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503 \ - --hash=sha256:e40d20ae1722d6b8ea6acf4597e789604649dcd9c295eb7361a28225bc2e9e12 \ - --hash=sha256:e4836c5645f40fbdc275e5670819bde5ab5f2e882290d304e3c6ddab1576a6d0 \ - --hash=sha256:e4ab69fa37f8090f2d971a5d2bc606c7401170dbdae083c393d6cbf439cb45b8 \ - --hash=sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50 \ - --hash=sha256:f0c8fe5aeadd8a0e5b0598f8a6ee3533135ca50fd3f20f130f9d72baf5c6ac58 \ - --hash=sha256:f2cb63c49bc96847c3bb8dca350970e4dcd19936f391cfdfd057dcb37c4fa97e \ - --hash=sha256:f4d4efb218264ecf0f8516196c9e2d1a0679d9fb3bb15df1155a35220062eba8 \ - --hash=sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3 \ - --hash=sha256:f83c971eb9d2358b6a18da51dc0ae00556ac7c73104dde16e9e14c15aaf685ca \ - --hash=sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439 \ - --hash=sha256:ff1fb2dfef035549565a4124998fadcb7a3d4957131ddf004a56edeb029626b3 - # via mypy -license-expression==30.4.4 \ - --hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \ - --hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd - # via cyclonedx-python-lib -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 - # via rich -markupsafe==3.0.3 \ - --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ - --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ - --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ - --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ - --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ - --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ - --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ - --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ - --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ - --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ - --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ - --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ - --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ - --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ - --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ - --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ - --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ - --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ - --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ - --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ - --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ - --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ - --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ - --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ - --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ - --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ - --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ - --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ - --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ - --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ - --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ - --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ - --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ - --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ - --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ - --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ - --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ - --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ - --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ - --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ - --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ - --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ - --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ - --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ - --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ - --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ - --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ - --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ - --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ - --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ - --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ - --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ - --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ - --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ - --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ - --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ - --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ - --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ - --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ - --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ - --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ - --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ - --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ - --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ - --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ - --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ - --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ - --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ - --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ - --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ - --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ - --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ - --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ - --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ - --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ - --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ - --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ - --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ - --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ - --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ - --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ - --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ - --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ - --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ - --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ - --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ - --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ - --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ - --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 - # via - # jinja2 - # werkzeug -mdurl==0.1.2 \ - --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ - --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba - # via markdown-it-py -msgpack==1.1.2 \ - --hash=sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2 \ - --hash=sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014 \ - --hash=sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931 \ - --hash=sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b \ - --hash=sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b \ - --hash=sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999 \ - --hash=sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029 \ - --hash=sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0 \ - --hash=sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9 \ - --hash=sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c \ - --hash=sha256:350ad5353a467d9e3b126d8d1b90fe05ad081e2e1cef5753f8c345217c37e7b8 \ - --hash=sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f \ - --hash=sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a \ - --hash=sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42 \ - --hash=sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e \ - --hash=sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f \ - --hash=sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7 \ - --hash=sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb \ - --hash=sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef \ - --hash=sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf \ - --hash=sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245 \ - --hash=sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794 \ - --hash=sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af \ - --hash=sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff \ - --hash=sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e \ - --hash=sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296 \ - --hash=sha256:67016ae8c8965124fdede9d3769528ad8284f14d635337ffa6a713a580f6c030 \ - --hash=sha256:6bde749afe671dc44893f8d08e83bf475a1a14570d67c4bb5cec5573463c8833 \ - --hash=sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939 \ - --hash=sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa \ - --hash=sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90 \ - --hash=sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c \ - --hash=sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717 \ - --hash=sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406 \ - --hash=sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a \ - --hash=sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251 \ - --hash=sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2 \ - --hash=sha256:94fd7dc7d8cb0a54432f296f2246bc39474e017204ca6f4ff345941d4ed285a7 \ - --hash=sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e \ - --hash=sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b \ - --hash=sha256:9fba231af7a933400238cb357ecccf8ab5d51535ea95d94fc35b7806218ff844 \ - --hash=sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9 \ - --hash=sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87 \ - --hash=sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b \ - --hash=sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c \ - --hash=sha256:a8f6e7d30253714751aa0b0c84ae28948e852ee7fb0524082e6716769124bc23 \ - --hash=sha256:ad09b984828d6b7bb52d1d1d0c9be68ad781fa004ca39216c8a1e63c0f34ba3c \ - --hash=sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e \ - --hash=sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620 \ - --hash=sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69 \ - --hash=sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f \ - --hash=sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68 \ - --hash=sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27 \ - --hash=sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46 \ - --hash=sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa \ - --hash=sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00 \ - --hash=sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9 \ - --hash=sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84 \ - --hash=sha256:ea5405c46e690122a76531ab97a079e184c0daf491e588592d6a23d3e32af99e \ - --hash=sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20 \ - --hash=sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e \ - --hash=sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162 - # via cachecontrol -mypy==1.19.1 \ - --hash=sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd \ - --hash=sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b \ - --hash=sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1 \ - --hash=sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba \ - --hash=sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b \ - --hash=sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045 \ - --hash=sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac \ - --hash=sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6 \ - --hash=sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a \ - --hash=sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24 \ - --hash=sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957 \ - --hash=sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042 \ - --hash=sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e \ - --hash=sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec \ - --hash=sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3 \ - --hash=sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718 \ - --hash=sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f \ - --hash=sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331 \ - --hash=sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1 \ - --hash=sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1 \ - --hash=sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13 \ - --hash=sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67 \ - --hash=sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2 \ - --hash=sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a \ - --hash=sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b \ - --hash=sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8 \ - --hash=sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376 \ - --hash=sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef \ - --hash=sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288 \ - --hash=sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75 \ - --hash=sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74 \ - --hash=sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250 \ - --hash=sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab \ - --hash=sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6 \ - --hash=sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247 \ - --hash=sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925 \ - --hash=sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e \ - --hash=sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e - # via python-basic-template (pyproject.toml) -mypy-extensions==1.1.0 \ - --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ - --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 - # via - # black - # mypy -nbformat==5.10.4 \ - --hash=sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a \ - --hash=sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b - # via nbstripout -nbstripout==0.8.2 \ - --hash=sha256:2876530eb684bf93a5b48fe6d92b2163f78d040721c76b37d5b9e1514d38fc69 \ - --hash=sha256:5f06f9138cb64daed3e91c5359ff0fff85bab4d0db7d72723be1da6f51b890ae - # via python-basic-template (pyproject.toml) -nodeenv==1.10.0 \ - --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ - --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb - # via pre-commit -packageurl-python==0.17.6 \ - --hash=sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25 \ - --hash=sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9 - # via cyclonedx-python-lib -packaging==25.0 \ - --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ - --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f - # via - # black - # build - # pip-audit - # pip-requirements-parser - # pytest -pathspec==1.0.3 \ - --hash=sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d \ - --hash=sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c - # via - # black - # mypy -pip-api==0.0.34 \ - --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ - --hash=sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625 - # via pip-audit -pip-audit==2.10.0 \ - --hash=sha256:16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f \ - --hash=sha256:427ea5bf61d1d06b98b1ae29b7feacc00288a2eced52c9c58ceed5253ef6c2a4 - # via python-basic-template (pyproject.toml) -pip-requirements-parser==32.0.1 \ - --hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \ - --hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3 - # via pip-audit -pip-tools==7.5.0 \ - --hash=sha256:30639f50961bb09f49d22f4389e8d7d990709677c094ce1114186b1f2e9b5821 \ - --hash=sha256:69758e4e5a65f160e315d74db46246fdbb30d549f1ed0c4236d057122c9b0f18 - # via python-basic-template (pyproject.toml) -platformdirs==4.5.1 \ - --hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \ - --hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31 - # via - # black - # jupyter-core - # pip-audit - # virtualenv -pluggy==1.6.0 \ - --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ - --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 - # via - # pytest - # pytest-cov -pre-commit==4.5.1 \ - --hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \ - --hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61 - # via python-basic-template (pyproject.toml) -py-serializable==2.1.0 \ - --hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \ - --hash=sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304 - # via cyclonedx-python-lib -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b - # via - # pytest - # rich -pyparsing==3.3.1 \ - --hash=sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82 \ - --hash=sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c - # via pip-requirements-parser -pyproject-hooks==1.2.0 \ - --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ - --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 - # via - # build - # pip-tools -pytest==9.0.2 \ - --hash=sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b \ - --hash=sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11 - # via - # pytest-beartype - # pytest-cov - # python-basic-template (pyproject.toml) -pytest-beartype==0.2.0 \ - --hash=sha256:9eeec379f812e0e5c9c1bb7e95d90a33a79c387034f2d735e03bc9aa0fd7cf3f \ - --hash=sha256:be6be04be4a143f965f21140d6661bab43dcd7c761d50cf6daa1b16848e5d2d0 - # via python-basic-template (pyproject.toml) -pytest-cov==7.0.0 \ - --hash=sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1 \ - --hash=sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861 - # via python-basic-template (pyproject.toml) -pytokens==0.3.0 \ - --hash=sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a \ - --hash=sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3 - # via black -pyyaml==6.0.3 \ - --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ - --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ - --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ - --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ - --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ - --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ - --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ - --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ - --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ - --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ - --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ - --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ - --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ - --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ - --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ - --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ - --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ - --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ - --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ - --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ - --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ - --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ - --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ - --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ - --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ - --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ - --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ - --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ - --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ - --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ - --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ - --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ - --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ - --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ - --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ - --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ - --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ - --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ - --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ - --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ - --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ - --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ - --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ - --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ - --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ - --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ - --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ - --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ - --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ - --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ - --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ - --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ - --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ - --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ - --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ - --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ - --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ - --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ - --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ - --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ - --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ - --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ - --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ - --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ - --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ - --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ - --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ - --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ - --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ - --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ - --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ - --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ - --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 - # via - # detect-secrets - # pre-commit -referencing==0.37.0 \ - --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ - --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 - # via - # jsonschema - # jsonschema-specifications -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via - # cachecontrol - # detect-secrets - # pip-audit - # python-basic-template (pyproject.toml) -rich==14.2.0 \ - --hash=sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4 \ - --hash=sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd - # via - # pip-audit - # python-basic-template (pyproject.toml) -rpds-py==0.30.0 \ - --hash=sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f \ - --hash=sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136 \ - --hash=sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3 \ - --hash=sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7 \ - --hash=sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65 \ - --hash=sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4 \ - --hash=sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169 \ - --hash=sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf \ - --hash=sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4 \ - --hash=sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2 \ - --hash=sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c \ - --hash=sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4 \ - --hash=sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3 \ - --hash=sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6 \ - --hash=sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7 \ - --hash=sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89 \ - --hash=sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85 \ - --hash=sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6 \ - --hash=sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa \ - --hash=sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb \ - --hash=sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6 \ - --hash=sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87 \ - --hash=sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856 \ - --hash=sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4 \ - --hash=sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f \ - --hash=sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53 \ - --hash=sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229 \ - --hash=sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad \ - --hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \ - --hash=sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db \ - --hash=sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038 \ - --hash=sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27 \ - --hash=sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00 \ - --hash=sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18 \ - --hash=sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083 \ - --hash=sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c \ - --hash=sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738 \ - --hash=sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898 \ - --hash=sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e \ - --hash=sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7 \ - --hash=sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08 \ - --hash=sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6 \ - --hash=sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551 \ - --hash=sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e \ - --hash=sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288 \ - --hash=sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df \ - --hash=sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0 \ - --hash=sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2 \ - --hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \ - --hash=sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0 \ - --hash=sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464 \ - --hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \ - --hash=sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404 \ - --hash=sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7 \ - --hash=sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139 \ - --hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \ - --hash=sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb \ - --hash=sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15 \ - --hash=sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff \ - --hash=sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed \ - --hash=sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6 \ - --hash=sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e \ - --hash=sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95 \ - --hash=sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d \ - --hash=sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950 \ - --hash=sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3 \ - --hash=sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5 \ - --hash=sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97 \ - --hash=sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e \ - --hash=sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e \ - --hash=sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b \ - --hash=sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd \ - --hash=sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad \ - --hash=sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8 \ - --hash=sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425 \ - --hash=sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221 \ - --hash=sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d \ - --hash=sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825 \ - --hash=sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51 \ - --hash=sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e \ - --hash=sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f \ - --hash=sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8 \ - --hash=sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f \ - --hash=sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d \ - --hash=sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07 \ - --hash=sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877 \ - --hash=sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31 \ - --hash=sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58 \ - --hash=sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94 \ - --hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \ - --hash=sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000 \ - --hash=sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1 \ - --hash=sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1 \ - --hash=sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7 \ - --hash=sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7 \ - --hash=sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40 \ - --hash=sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d \ - --hash=sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0 \ - --hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \ - --hash=sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f \ - --hash=sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a \ - --hash=sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7 \ - --hash=sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419 \ - --hash=sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8 \ - --hash=sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a \ - --hash=sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9 \ - --hash=sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be \ - --hash=sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed \ - --hash=sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a \ - --hash=sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d \ - --hash=sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324 \ - --hash=sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f \ - --hash=sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2 \ - --hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f \ - --hash=sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5 - # via - # jsonschema - # referencing -ruff==0.14.11 \ - --hash=sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1 \ - --hash=sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841 \ - --hash=sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa \ - --hash=sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe \ - --hash=sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9 \ - --hash=sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0 \ - --hash=sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f \ - --hash=sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6 \ - --hash=sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984 \ - --hash=sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491 \ - --hash=sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3 \ - --hash=sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe \ - --hash=sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7 \ - --hash=sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf \ - --hash=sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2 \ - --hash=sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e \ - --hash=sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958 \ - --hash=sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e \ - --hash=sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6 - # via python-basic-template (pyproject.toml) -sortedcontainers==2.4.0 \ - --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ - --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 - # via cyclonedx-python-lib -tomli==2.4.0 \ - --hash=sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729 \ - --hash=sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b \ - --hash=sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d \ - --hash=sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df \ - --hash=sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576 \ - --hash=sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d \ - --hash=sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1 \ - --hash=sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a \ - --hash=sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e \ - --hash=sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc \ - --hash=sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702 \ - --hash=sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6 \ - --hash=sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd \ - --hash=sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4 \ - --hash=sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776 \ - --hash=sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a \ - --hash=sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66 \ - --hash=sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87 \ - --hash=sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2 \ - --hash=sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f \ - --hash=sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475 \ - --hash=sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f \ - --hash=sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95 \ - --hash=sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9 \ - --hash=sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3 \ - --hash=sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9 \ - --hash=sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76 \ - --hash=sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da \ - --hash=sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8 \ - --hash=sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51 \ - --hash=sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86 \ - --hash=sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8 \ - --hash=sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0 \ - --hash=sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b \ - --hash=sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1 \ - --hash=sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e \ - --hash=sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d \ - --hash=sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c \ - --hash=sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867 \ - --hash=sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a \ - --hash=sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c \ - --hash=sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0 \ - --hash=sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4 \ - --hash=sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614 \ - --hash=sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132 \ - --hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \ - --hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087 - # via pip-audit -tomli-w==1.2.0 \ - --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ - --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 - # via pip-audit -traitlets==5.14.3 \ - --hash=sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7 \ - --hash=sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f - # via - # jupyter-core - # nbformat -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 - # via - # cyclonedx-python-lib - # mypy - # referencing -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 - # via - # python-basic-template (pyproject.toml) - # requests -virtualenv==20.36.1 \ - --hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \ - --hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba - # via pre-commit -werkzeug==3.1.5 \ - --hash=sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc \ - --hash=sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67 - # via - # flask - # python-basic-template (pyproject.toml) -wheel==0.45.1 \ - --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ - --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 - # via pip-tools - -# WARNING: The following packages were not pinned, but pip requires them to be -# pinned when the requirements file includes hashes and the requirement is not -# satisfied by a package already installed. Consider using the --allow-unsafe flag. -# pip -# setuptools diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d8c02b0d..00000000 --- a/requirements.txt +++ /dev/null @@ -1,262 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile --cert=None --client-cert=None --generate-hashes --index-url=None --output-file=requirements.txt --pip-args=None --strip-extras pyproject.toml -# -blinker==1.9.0 \ - --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ - --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc - # via flask -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 - # via requests -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 - # via requests -click==8.3.1 \ - --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ - --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 - # via flask -filelock==3.20.3 \ - --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ - --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 - # via python-basic-template (pyproject.toml) -flask==2.3.3 \ - --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ - --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b - # via python-basic-template (pyproject.toml) -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 - # via requests -itsdangerous==2.2.0 \ - --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ - --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 - # via flask -jinja2==3.1.6 \ - --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ - --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via flask -markupsafe==3.0.3 \ - --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ - --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ - --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ - --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ - --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ - --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ - --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ - --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ - --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ - --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ - --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ - --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ - --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ - --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ - --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ - --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ - --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ - --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ - --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ - --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ - --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ - --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ - --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ - --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ - --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ - --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ - --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ - --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ - --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ - --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ - --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ - --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ - --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ - --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ - --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ - --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ - --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ - --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ - --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ - --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ - --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ - --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ - --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ - --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ - --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ - --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ - --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ - --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ - --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ - --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ - --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ - --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ - --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ - --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ - --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ - --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ - --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ - --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ - --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ - --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ - --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ - --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ - --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ - --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ - --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ - --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ - --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ - --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ - --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ - --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ - --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ - --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ - --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ - --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ - --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ - --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ - --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ - --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ - --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ - --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ - --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ - --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ - --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ - --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ - --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ - --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ - --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ - --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ - --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 - # via - # jinja2 - # werkzeug -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via python-basic-template (pyproject.toml) -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 - # via - # python-basic-template (pyproject.toml) - # requests -werkzeug==3.1.5 \ - --hash=sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc \ - --hash=sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67 - # via - # flask - # python-basic-template (pyproject.toml) diff --git a/scripts/dependency.sh b/scripts/dependency.sh index 51b4ebfe..681900d5 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -1,207 +1,209 @@ #!/usr/bin/env bash set -euo pipefail -# ----------------------------------------------------------------------------- +# ============================================================================= +# uv Dependency Management Script +# ============================================================================= +# A modern dependency management helper using uv for fast, reliable builds +# +# Usage: +# ./scripts/dependency.sh # Setup dev environment (default) +# ./scripts/dependency.sh --prod # Production environment +# ./scripts/dependency.sh --no-sync # Compile only, skip sync +# ./scripts/dependency.sh --check # Check for package updates +# ./scripts/dependency.sh --update # Check and auto-update packages +# ./scripts/dependency.sh --update -y # Update without prompting +# +# This script replaces the previous pip-tools workflow with uv, which provides: +# - Faster dependency resolution +# - Simpler, more transparent lock files +# - Better compatibility with modern Python packaging +# +# ============================================================================= + # Configuration -# ----------------------------------------------------------------------------- -# Use environment variables if they exist, otherwise set sensible defaults. export VENV_DIR="${VENV_DIR:-.venv}" export PYPROJECT="${PYPROJECT:-pyproject.toml}" -export PROD_LOCK="${PROD_LOCK:-requirements.txt}" -export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" -export SLIM_LOCK="${SLIM_LOCK:-requirements-slim.txt}" - -# ----------------------------------------------------------------------------- -# Script Info -# ----------------------------------------------------------------------------- -# Hybrid dependency management script: -# Compiles lockfiles first, then installs, to ensure a robust workflow. -# -# Usage: -# ./scripts/dependency.sh # Compile and install dev (which includes all) -# ./scripts/dependency.sh --prod # Compile and install only prod (full production) -# ./scripts/dependency.sh --dev # Compile and install only dev -# ./scripts/dependency.sh --slim # Compile and install only slim (core only) - -# ----------------------------------------------------------------------------- -# UI Functions -# ----------------------------------------------------------------------------- -# Colors +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Colors for output YELLOW='\033[1;33m' GREEN='\033[1;32m' BLUE='\033[0;34m' RED='\033[0;31m' RESET='\033[0m' -# Helper functions for printing formatted output +# Helper functions info() { echo -e "${BLUE}โ„น๏ธ $1${RESET}"; } step() { echo -e "\n${YELLOW}$1${RESET}"; } success() { echo -e "${GREEN}โœ… $1${RESET}"; } error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } -# ----------------------------------------------------------------------------- # Argument Parsing -# ----------------------------------------------------------------------------- -INSTALL_PROD=false -INSTALL_DEV=false -INSTALL_SLIM=false - -# Default to dev if no arguments are provided -if [ $# -eq 0 ]; then - INSTALL_DEV=true -else - for arg in "$@"; do - case $arg in - --prod) - INSTALL_PROD=true - ;; - --dev) - INSTALL_DEV=true - ;; - --slim) - INSTALL_SLIM=true - ;; - *) - error "Unknown option: $arg. Usage: $0 [--prod] [--dev] [--slim]" - ;; - esac - done +PROD_MODE=false +SKIP_SYNC=false +CHECK_UPDATES=false +AUTO_UPDATE=false +UPDATE_YES=false + +while [ $# -gt 0 ]; do + case "$1" in + --prod) + PROD_MODE=true + shift + ;; + --no-sync) + SKIP_SYNC=true + shift + ;; + --check) + CHECK_UPDATES=true + shift + ;; + --update) + AUTO_UPDATE=true + CHECK_UPDATES=true + shift + # Check for -y flag + if [ $# -gt 0 ] && [ "$1" = "-y" ]; then + UPDATE_YES=true + shift + fi + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --prod Production environment (no dev tools)" + echo " --no-sync Lock only, skip sync" + echo " --check Check for package updates" + echo " --update Check and update packages (interactive)" + echo " --update -y Update packages without prompting" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + error "Unknown option: $1. Use --help for usage information." + ;; + esac +done + +# Check if uv is installed +step "Checking for uv installation..." +if ! command -v uv &> /dev/null; then + error "uv is not installed. Please install it with: https://docs.astral.sh/uv/getting-started/installation/" fi +success "uv is installed ($(uv --version))" -# ----------------------------------------------------------------------------- -# Environment Setup -# ----------------------------------------------------------------------------- -step "Activating virtualenv at '${VENV_DIR}'..." -if [ -f "${VENV_DIR}/bin/activate" ]; then - # shellcheck disable=SC1090 - source "${VENV_DIR}/bin/activate" - success "Virtual environment activated." -else - info "Creating virtual environment at '${VENV_DIR}'..." - python3 -m venv "${VENV_DIR}" - success "Virtual environment created." - # shellcheck disable=SC1090 - source "${VENV_DIR}/bin/activate" - success "Virtual environment activated." -fi +# ============================================================================= +# HELPER FUNCTIONS FOR UPDATES +# ============================================================================= -step "Ensuring pip-tools is installed..." -if ! pip show pip-tools &>/dev/null; then - info "Installing pip-tools..." - pip install pip-tools - success "pip-tools installed." -else - success "pip-tools is already installed." -fi +check_and_update_deps() { + local auto_update=$1 + local force_yes=$2 + + step "Checking for package updates using Python script..." + + if [ ! -f "$SCRIPTS_DIR/update_deps.py" ]; then + error "update_deps.py not found at $SCRIPTS_DIR/update_deps.py" + fi + + # Run the update script with appropriate flags + local python_cmd="python $SCRIPTS_DIR/update_deps.py" + + if [ "$auto_update" = true ]; then + if [ "$force_yes" = true ]; then + $python_cmd --yes + else + $python_cmd + fi + else + # Just check, don't update + info "Checking for available updates..." + $python_cmd + info "To apply updates, run: ./scripts/dependency.sh --update" + fi +} # ============================================================================= -# MAJOR CHANGE: The script is now split into a COMPILE phase and an INSTALL phase. +# EARLY EXIT FOR UPDATE OPERATIONS # ============================================================================= -# Check if there are extra optional dependencies -HAS_EXTRA_DEPS=false -if python3 -c "import tomllib; d = tomllib.load(open('${PYPROJECT}', 'rb')); deps = d.get('project', {}).get('optional-dependencies', {}).get('extra', []); exit(0 if len(deps) > 0 else 1)" 2>/dev/null; then - HAS_EXTRA_DEPS=true +if [ "$CHECK_UPDATES" = true ]; then + check_and_update_deps "$AUTO_UPDATE" "$UPDATE_YES" + exit 0 fi -# ----------------------------------------------------------------------------- -# PHASE 1: COMPILE ALL REQUIRED LOCKFILES -# ----------------------------------------------------------------------------- -# This block runs first to ensure our lockfiles are valid before we try to -# install from them. This prevents errors if a file was manually edited. -# -# Logic: -# - Always compile requirements.txt and requirements-dev.txt -# - If extra deps exist: requirements.txt includes extra, requirements-slim.txt is core only -# - If no extra deps: requirements.txt is core, no slim file is created - -step "PHASE 1: Compiling lockfiles from '${PYPROJECT}'..." - -if [ "$HAS_EXTRA_DEPS" = true ]; then - info "Extra dependencies detected. Compiling production lockfile with extras: '${PROD_LOCK}'" - pip-compile "${PYPROJECT}" \ - --extra=extra \ - --output-file="${PROD_LOCK}" \ - --generate-hashes \ - --strip-extras -else - info "No extra dependencies. Compiling base production lockfile: '${PROD_LOCK}'" - pip-compile "${PYPROJECT}" \ - --output-file="${PROD_LOCK}" \ - --generate-hashes \ - --strip-extras -fi - -info "Compiling development lockfile: '${DEV_LOCK}'" -if [ "$HAS_EXTRA_DEPS" = true ]; then - pip-compile "${PYPROJECT}" \ - --extra=dev \ - --extra=extra \ - --output-file="${DEV_LOCK}" \ - --generate-hashes \ - --strip-extras +# Create or activate virtual environment +step "Setting up virtual environment at '${VENV_DIR}'..." +if [ ! -d "${VENV_DIR}" ]; then + info "Creating new virtual environment..." + uv venv "${VENV_DIR}" + success "Virtual environment created." else - pip-compile "${PYPROJECT}" \ - --extra=dev \ - --output-file="${DEV_LOCK}" \ - --generate-hashes \ - --strip-extras + success "Virtual environment exists." fi -# Only compile slim if there are extra dependencies -if [ "$HAS_EXTRA_DEPS" = true ]; then - info "Compiling slim lockfile (core only): '${SLIM_LOCK}'" - pip-compile "${PYPROJECT}" \ - --output-file="${SLIM_LOCK}" \ - --generate-hashes \ - --strip-extras +# Ensure the venv is valid +if [ ! -f "${VENV_DIR}/bin/activate" ] && [ ! -f "${VENV_DIR}/Scripts/activate" ]; then + error "Virtual environment is broken. Remove ${VENV_DIR} and rerun this script." fi -success "All lockfiles compiled." - -# ----------------------------------------------------------------------------- -# PHASE 2: INSTALL DEPENDENCIES FROM THE (NOW VERIFIED) LOCKFILES -# ----------------------------------------------------------------------------- -# Now that we know the lockfiles are correct, we can safely install from them. - -step "PHASE 2: Installing dependencies..." - -if [ "$INSTALL_PROD" = true ]; then - info "Installing production dependencies from '${PROD_LOCK}'..." - pip install --require-hashes -r "${PROD_LOCK}" -fi +# ============================================================================= +# PHASE 1: LOCK DEPENDENCIES +# ============================================================================= +step "PHASE 1: Locking dependencies using uv..." -if [ "$INSTALL_DEV" = true ]; then - info "Installing development dependencies from '${DEV_LOCK}'..." - pip install --require-hashes -r "${DEV_LOCK}" +if [ "$PROD_MODE" = true ]; then + info "Production mode: Locking production dependencies only (no dev tools)" + uv lock --no-dev + success "Production dependencies locked to uv.lock" +else + info "Development mode: Locking all dependencies (including dev tools)" + uv lock + success "All dependencies locked to uv.lock" fi -if [ "$INSTALL_SLIM" = true ]; then - info "Installing slim dependencies from '${SLIM_LOCK}'..." - # The slim requirements are the core ones. - pip install --require-hashes -r "${SLIM_LOCK}" +# ============================================================================= +# PHASE 2: SYNC ENVIRONMENT +# ============================================================================= +if [ "$SKIP_SYNC" = false ]; then + step "PHASE 2: Syncing environment with uv..." + + if [ "$PROD_MODE" = true ]; then + info "Installing production dependencies only (no dev packages)..." + uv sync --no-dev + success "Production environment synced." + else + info "Installing all dependencies (core + dev)..." + uv sync + success "Development environment synced." + fi +else + step "PHASE 2: Skipped (--no-sync flag provided)" + info "Run 'uv sync' manually when ready to install dependencies." fi -success "All required dependencies installed." - -# ----------------------------------------------------------------------------- -# Final Verification -# ----------------------------------------------------------------------------- -step "Verifying environment consistency..." -pip check -success "Environment check passed." - -# --- Final Summary --- +# Final Summary echo success "Dependency setup complete!" -echo " โ€ข All lockfiles compiled" -if [ $# -eq 0 ]; then - echo " โ€ข Development dependencies (including all) installed from '${DEV_LOCK}'" +if [ "$PROD_MODE" = true ]; then + echo " โ€ข Production dependencies locked and synced" else - [ "$INSTALL_PROD" = true ] && echo " โ€ข Production dependencies installed from '${PROD_LOCK}'" - [ "$INSTALL_DEV" = true ] && echo " โ€ข Development dependencies installed from '${DEV_LOCK}'" - [ "$INSTALL_SLIM" = true ] && echo " โ€ข Slim dependencies installed from '${SLIM_LOCK}'" + echo " โ€ข All dependencies locked and synced" + echo " โ€ข Development tools installed" fi -if [ "$HAS_EXTRA_DEPS" = true ]; then - echo " โ€ข Slim requirements file available: ${SLIM_LOCK}" +echo " โ€ข Lock file: uv.lock" +echo +info "Next steps:" +if [ "$SKIP_SYNC" = false ]; then + echo " โ€ข Run code: uv run python script.py" + echo " โ€ข Run tests: uv run pytest" + echo " โ€ข Type check: uv run mypy ." + echo " โ€ข Check for updates: ./scripts/dependency.sh --check" + echo " โ€ข Update packages: ./scripts/dependency.sh --update" +else + echo " โ€ข Sync environment: uv sync" + echo " โ€ข Then use: uv run python script.py" fi diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..395d27a7 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1257 @@ +version = 1 +revision = 3 +requires-python = ">=3.12, <4.0" + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "black" +version = "25.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" }, + { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" }, + { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" }, + { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" }, + { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" }, + { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" }, + { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, +] + +[[package]] +name = "cachecontrol" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, +] + +[package.optional-dependencies] +filecache = [ + { name = "filelock" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[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/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 = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[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 = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[[package]] +name = "cyclonedx-python-lib" +version = "11.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "license-expression" }, + { name = "packageurl-python" }, + { name = "py-serializable" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/ed/54ecfa25fc145c58bf4f98090f7b6ffe5188d0759248c57dde44427ea239/cyclonedx_python_lib-11.6.0.tar.gz", hash = "sha256:7fb85a4371fa3a203e5be577ac22b7e9a7157f8b0058b7448731474d6dea7bf0", size = 1408147, upload-time = "2025-12-02T12:28:46.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/1b/534ad8a5e0f9470522811a8e5a9bc5d328fb7738ba29faf357467a4ef6d0/cyclonedx_python_lib-11.6.0-py3-none-any.whl", hash = "sha256:94f4aae97db42a452134dafdddcfab9745324198201c4777ed131e64c8380759", size = 511157, upload-time = "2025-12-02T12:28:44.158Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "detect-secrets" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/67/382a863fff94eae5a0cf05542179169a1c49a4c8784a9480621e2066ca7d/detect_secrets-1.5.0.tar.gz", hash = "sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a", size = 97351, upload-time = "2024-05-06T17:46:19.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/5e/4f5fe4b89fde1dc3ed0eb51bd4ce4c0bca406246673d370ea2ad0c58d747/detect_secrets-1.5.0-py3-none-any.whl", hash = "sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060", size = 120341, upload-time = "2024-05-06T17:46:16.628Z" }, +] + +[[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 = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "flask" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/4ace17e37abd9c21715dea5ee11774a25e404c486a7893fa18e764326ead/flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc", size = 672756, upload-time = "2023-08-21T19:52:35.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b", size = 96112, upload-time = "2023-08-21T19:52:33.115Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[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 = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[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 = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, + { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, + { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, + { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, +] + +[[package]] +name = "license-expression" +version = "30.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boolean-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[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/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 = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nbstripout" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nbformat" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/09/cea5360a0788f94337d1b65c313ff0a448fb6a444b65cab89378eb8f7d5c/nbstripout-0.8.2.tar.gz", hash = "sha256:2876530eb684bf93a5b48fe6d92b2163f78d040721c76b37d5b9e1514d38fc69", size = 27747, upload-time = "2025-11-16T17:38:55.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.py3-none-any.whl", hash = "sha256:5f06f9138cb64daed3e91c5359ff0fff85bab4d0db7d72723be1da6f51b890ae", size = 17122, upload-time = "2025-11-16T17:38:54.164Z" }, +] + +[[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 = "packageurl-python" +version = "0.17.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" }, +] + +[[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 = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "pip" +version = "25.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014, upload-time = "2025-10-25T00:55:41.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" }, +] + +[[package]] +name = "pip-api" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" }, +] + +[[package]] +name = "pip-audit" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachecontrol", extra = ["filecache"] }, + { name = "cyclonedx-python-lib" }, + { name = "packaging" }, + { name = "pip-api" }, + { name = "pip-requirements-parser" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "rich" }, + { name = "tomli" }, + { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/89/0e999b413facab81c33d118f3ac3739fd02c0622ccf7c4e82e37cebd8447/pip_audit-2.10.0.tar.gz", hash = "sha256:427ea5bf61d1d06b98b1ae29b7feacc00288a2eced52c9c58ceed5253ef6c2a4", size = 53776, upload-time = "2025-12-01T23:42:40.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl", hash = "sha256:16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f", size = 61518, upload-time = "2025-12-01T23:42:39.193Z" }, +] + +[[package]] +name = "pip-requirements-parser" +version = "32.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" }, +] + +[[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 = "py-serializable" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, +] + +[[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 = "pyparsing" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, +] + +[[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-beartype" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/69/954ee438f1c72f4247b09381cd07a7d776ebdb4f6c0359b3b971676a2eef/pytest_beartype-0.2.0.tar.gz", hash = "sha256:9eeec379f812e0e5c9c1bb7e95d90a33a79c387034f2d735e03bc9aa0fd7cf3f", size = 5871, upload-time = "2024-10-31T17:52:35.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/69/87efc2482aab172a32e4147eb5666ebc2e4c875e17d3104878553720a83a/pytest_beartype-0.2.0-py3-none-any.whl", hash = "sha256:be6be04be4a143f965f21140d6661bab43dcd7c761d50cf6daa1b16848e5d2d0", size = 4940, upload-time = "2024-10-31T17:52:33.528Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { 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 = "python-basic-template" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "filelock" }, + { name = "flask" }, + { name = "requests" }, + { name = "urllib3" }, + { name = "werkzeug" }, +] + +[package.optional-dependencies] +dev = [ + { name = "beartype" }, + { name = "black" }, + { name = "detect-secrets" }, + { name = "mypy" }, + { name = "nbstripout" }, + { name = "pip-audit" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-beartype" }, + { name = "pytest-cov" }, + { name = "rich" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "beartype", marker = "extra == 'dev'" }, + { name = "black", marker = "extra == 'dev'" }, + { name = "detect-secrets", marker = "extra == 'dev'" }, + { name = "filelock", specifier = ">=3.20.1,<4.0.0" }, + { name = "flask", specifier = ">=2.2.0,<3.0.0" }, + { name = "mypy", marker = "extra == 'dev'" }, + { name = "nbstripout", marker = "extra == 'dev'" }, + { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.9.0" }, + { name = "pre-commit", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, + { name = "pytest-beartype", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "requests" }, + { name = "rich", marker = "extra == 'dev'" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "urllib3", specifier = ">=2.6.3,<3.0.0" }, + { name = "werkzeug", specifier = ">=3.1.5,<4.0.0" }, +] +provides-extras = ["dev", "extra"] + +[[package]] +name = "pytokens" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, +] + +[[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/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 = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, + { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[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 = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] From 49d36bb0a0e41fedfcb6463562cac4df6376290d Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 14 Jan 2026 11:30:41 +0100 Subject: [PATCH 090/199] Pin dependencies to specific versions for stability and update dependency management script --- pyproject.toml | 34 +++++------ scripts/dependency.sh | 134 ++++++++++++++++++++++++++++++++++++++---- uv.lock | 41 ++++++------- 3 files changed, 159 insertions(+), 50 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f8c7a2ce..9c9d8a77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,11 +28,11 @@ classifiers = [ # These are compiled into requirements-slim.txt (core only) and included in all other requirement files. # Add only dependencies that are needed in production environments. dependencies = [ - "flask>=2.2.0,<3.0.0", # Example dependency, replace with your own - "requests", - "urllib3>=2.6.3,<3.0.0", - "werkzeug>=3.1.5,<4.0.0", - "filelock>=3.20.1,<4.0.0", + "flask==3.1.2", # Example dependency, replace with your own + "requests==2.32.5", + "urllib3==2.6.3", + "werkzeug==3.1.5", + "filelock==3.20.3", ] # Optional dependency groups for different environments and use cases. @@ -42,18 +42,18 @@ dependencies = [ # Includes code quality tools, testing frameworks, and development utilities. # Compiled into requirements-dev.txt (core + extra + dev). dev = [ - "pre-commit", - "detect-secrets", - "nbstripout", - "pytest>=7.0", - "pytest-cov", - "black", - "mypy", - "ruff", - "beartype", - "pytest-beartype", - "pip-audit>=2.9.0", - "rich", + "pre-commit==4.5.1", + "detect-secrets==1.5.0", + "nbstripout==0.8.2", + "pytest==9.0.2", + "pytest-cov==7.0.0", + "black==25.12.0", + "mypy==1.19.1", + "ruff==0.14.11", + "beartype==0.22.9", + "pytest-beartype==0.2.0", + "pip-audit==2.10.0", + "rich==14.2.0", ] # Additional production dependencies. diff --git a/scripts/dependency.sh b/scripts/dependency.sh index 681900d5..9a010095 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -99,31 +99,139 @@ success "uv is installed ($(uv --version))" # HELPER FUNCTIONS FOR UPDATES # ============================================================================= +get_latest_version() { + local package=$1 + + # Fetch latest version from PyPI API + local json_response=$(curl -s "https://pypi.org/pypi/${package}/json" 2>/dev/null) + + if [ -z "$json_response" ]; then + return 1 + fi + + # Extract version from JSON (basic extraction, no jq dependency) + local version=$(echo "$json_response" | grep -oP '"version":\s*"\K[^"]+' | head -1) + + if [ -n "$version" ]; then + echo "$version" + return 0 + fi + return 1 +} + +parse_pyproject() { + # Extract full package specifications from dependencies sections only + # Matches entries like: "package", "package>=1.0", "package>=1.0,<2.0", etc. + # Only in 'dependencies' and 'dev' sections, skipping classifiers + awk ' + /^\[project\]$/ {in_deps=1; next} + /^\[project.optional-dependencies/ {in_deps=2; next} + /^\[/ && !/^\[project/ {in_deps=0; next} + (in_deps==1 || in_deps==2) && /^\s*"[a-zA-Z0-9_\-\.@]+/ && !/^dependencies|^dev/ { + # Skip lines that contain these patterns (from classifiers and config) + if ($0 !~ /::/) { + match($0, /"([^"]+)"/, arr) + if (arr[1]) print arr[1] + } + } + ' "$PYPROJECT" +} + check_and_update_deps() { local auto_update=$1 local force_yes=$2 - step "Checking for package updates using Python script..." + step "Checking for package updates..." - if [ ! -f "$SCRIPTS_DIR/update_deps.py" ]; then - error "update_deps.py not found at $SCRIPTS_DIR/update_deps.py" + if [ ! -f "$PYPROJECT" ]; then + error "pyproject.toml not found!" fi - # Run the update script with appropriate flags - local python_cmd="python $SCRIPTS_DIR/update_deps.py" + # Parse current packages + local packages=$(parse_pyproject) + local pkg_count=$(echo "$packages" | wc -l) + info "Found $pkg_count pinned packages" + echo + + # Check for updates - use temp files instead of associative array for compatibility + local updates_file=$(mktemp) + trap "rm -f $updates_file" RETURN - if [ "$auto_update" = true ]; then - if [ "$force_yes" = true ]; then - $python_cmd --yes + while read -r package_spec; do + # Extract package name (everything before version operators) + local package_name=$(echo "$package_spec" | sed 's/[><=~!@].*//') + + printf "Checking $package_name (current: $package_spec)... " + + local latest_version + latest_version=$(get_latest_version "$package_name" 2>/dev/null) || latest_version="" + + if [ -n "$latest_version" ]; then + printf "โ†’ $latest_version (UPDATE AVAILABLE)\n" + # Store: original_spec|package_name|new_version + echo "$package_spec|$package_name|$latest_version" >> "$updates_file" else - $python_cmd + printf "โœ“ up to date\n" fi - else - # Just check, don't update - info "Checking for available updates..." - $python_cmd + done <<< "$packages" + + echo + local update_count=0 + if [ -f "$updates_file" ] && [ -s "$updates_file" ]; then + update_count=$(wc -l < "$updates_file") + fi + info "Updates available: $update_count" + + if [ "$update_count" -eq 0 ]; then + success "All packages are up to date!" + return 0 + fi + + # Show updates available + info "Updates found:" + while IFS='|' read -r orig_spec pkg_name new_version; do + info " $pkg_name โ†’ $new_version" + done < "$updates_file" + echo + + if [ "$auto_update" = false ]; then info "To apply updates, run: ./scripts/dependency.sh --update" + return 0 fi + + # Ask user if they want to update (unless --yes flag is used) + if [ "$force_yes" = false ]; then + read -p "Do you want to update pyproject.toml with these versions? (y/N): " -r response + response=${response,,} # Convert to lowercase + if [[ ! "$response" =~ ^(y|yes)$ ]]; then + info "Update cancelled." + return 0 + fi + else + info "Auto-updating due to --yes flag..." + fi + + # Update pyproject.toml + local updated_content + updated_content=$(cat "$PYPROJECT") + + while IFS='|' read -r orig_spec pkg_name new_version; do + # Escape special regex characters in the original spec + local escaped_spec=$(echo "$orig_spec" | sed 's/[[\.*^$/]/\\&/g') + # Replace the old spec with package name pinned to new version (==) + updated_content=$(echo "$updated_content" | sed "s/\"${escaped_spec}\"/\"${pkg_name}==${new_version}\"/g") + done < "$updates_file" + + echo "$updated_content" > "$PYPROJECT" + success "Updated $update_count packages in pyproject.toml" + + # Run uv lock to update the lock file + step "Updating uv.lock..." + uv lock + success "Lock file updated" + + echo + info "Done! You may want to run 'uv sync' to install the updated packages." } # ============================================================================= diff --git a/uv.lock b/uv.lock index 395d27a7..b96fba89 100644 --- a/uv.lock +++ b/uv.lock @@ -325,18 +325,19 @@ wheels = [ [[package]] name = "flask" -version = "2.3.3" +version = "3.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, { name = "click" }, { name = "itsdangerous" }, { name = "jinja2" }, + { name = "markupsafe" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/b7/4ace17e37abd9c21715dea5ee11774a25e404c486a7893fa18e764326ead/flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc", size = 672756, upload-time = "2023-08-21T19:52:35.012Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b", size = 96112, upload-time = "2023-08-21T19:52:33.115Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, ] [[package]] @@ -916,23 +917,23 @@ dev = [ [package.metadata] requires-dist = [ - { name = "beartype", marker = "extra == 'dev'" }, - { name = "black", marker = "extra == 'dev'" }, - { name = "detect-secrets", marker = "extra == 'dev'" }, - { name = "filelock", specifier = ">=3.20.1,<4.0.0" }, - { name = "flask", specifier = ">=2.2.0,<3.0.0" }, - { name = "mypy", marker = "extra == 'dev'" }, - { name = "nbstripout", marker = "extra == 'dev'" }, - { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.9.0" }, - { name = "pre-commit", marker = "extra == 'dev'" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, - { name = "pytest-beartype", marker = "extra == 'dev'" }, - { name = "pytest-cov", marker = "extra == 'dev'" }, - { name = "requests" }, - { name = "rich", marker = "extra == 'dev'" }, - { name = "ruff", marker = "extra == 'dev'" }, - { name = "urllib3", specifier = ">=2.6.3,<3.0.0" }, - { name = "werkzeug", specifier = ">=3.1.5,<4.0.0" }, + { name = "beartype", marker = "extra == 'dev'", specifier = "==0.22.9" }, + { name = "black", marker = "extra == 'dev'", specifier = "==25.12.0" }, + { name = "detect-secrets", marker = "extra == 'dev'", specifier = "==1.5.0" }, + { name = "filelock", specifier = "==3.20.3" }, + { name = "flask", specifier = "==3.1.2" }, + { name = "mypy", marker = "extra == 'dev'", specifier = "==1.19.1" }, + { name = "nbstripout", marker = "extra == 'dev'", specifier = "==0.8.2" }, + { name = "pip-audit", marker = "extra == 'dev'", specifier = "==2.10.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = "==4.5.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = "==9.0.2" }, + { name = "pytest-beartype", marker = "extra == 'dev'", specifier = "==0.2.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = "==7.0.0" }, + { name = "requests", specifier = "==2.32.5" }, + { name = "rich", marker = "extra == 'dev'", specifier = "==14.2.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.14.11" }, + { name = "urllib3", specifier = "==2.6.3" }, + { name = "werkzeug", specifier = "==3.1.5" }, ] provides-extras = ["dev", "extra"] From 2158fe27f1b5bb92b8a5c97400c2516307d4b4b2 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 14 Jan 2026 11:46:41 +0100 Subject: [PATCH 091/199] Refactor dependency management script to handle major version updates and improve update prompts --- scripts/dependency.sh | 145 +++++++++++++++++++++++++++++++++--------- 1 file changed, 115 insertions(+), 30 deletions(-) diff --git a/scripts/dependency.sh b/scripts/dependency.sh index 9a010095..24f64039 100755 --- a/scripts/dependency.sh +++ b/scripts/dependency.sh @@ -12,7 +12,6 @@ set -euo pipefail # ./scripts/dependency.sh --no-sync # Compile only, skip sync # ./scripts/dependency.sh --check # Check for package updates # ./scripts/dependency.sh --update # Check and auto-update packages -# ./scripts/dependency.sh --update -y # Update without prompting # # This script replaces the previous pip-tools workflow with uv, which provides: # - Faster dependency resolution @@ -44,7 +43,6 @@ PROD_MODE=false SKIP_SYNC=false CHECK_UPDATES=false AUTO_UPDATE=false -UPDATE_YES=false while [ $# -gt 0 ]; do case "$1" in @@ -64,11 +62,6 @@ while [ $# -gt 0 ]; do AUTO_UPDATE=true CHECK_UPDATES=true shift - # Check for -y flag - if [ $# -gt 0 ] && [ "$1" = "-y" ]; then - UPDATE_YES=true - shift - fi ;; -h|--help) echo "Usage: $0 [OPTIONS]" @@ -78,7 +71,6 @@ while [ $# -gt 0 ]; do echo " --no-sync Lock only, skip sync" echo " --check Check for package updates" echo " --update Check and update packages (interactive)" - echo " --update -y Update packages without prompting" echo " -h, --help Show this help message" exit 0 ;; @@ -99,6 +91,32 @@ success "uv is installed ($(uv --version))" # HELPER FUNCTIONS FOR UPDATES # ============================================================================= +get_major_version() { + local version=$1 + echo "$version" | cut -d. -f1 +} + +get_current_version_from_spec() { + local spec=$1 + # Extract version from spec like "package>=1.2.3,<2.0.0" + # Gets the first version number found + echo "$spec" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+|[0-9]+\.[0-9]+' | head -1 +} + +is_major_version_bump() { + local current_version=$1 + local new_version=$2 + + if [ -z "$current_version" ]; then + return 1 # No current version, so not a bump + fi + + local current_major=$(get_major_version "$current_version") + local new_major=$(get_major_version "$new_version") + + [ "$current_major" != "$new_major" ] +} + get_latest_version() { local package=$1 @@ -139,7 +157,6 @@ parse_pyproject() { check_and_update_deps() { local auto_update=$1 - local force_yes=$2 step "Checking for package updates..." @@ -155,21 +172,30 @@ check_and_update_deps() { # Check for updates - use temp files instead of associative array for compatibility local updates_file=$(mktemp) - trap "rm -f $updates_file" RETURN + local major_updates_file=$(mktemp) + trap "rm -f $updates_file $major_updates_file" RETURN while read -r package_spec; do # Extract package name (everything before version operators) local package_name=$(echo "$package_spec" | sed 's/[><=~!@].*//') + local current_version=$(get_current_version_from_spec "$package_spec") printf "Checking $package_name (current: $package_spec)... " local latest_version latest_version=$(get_latest_version "$package_name" 2>/dev/null) || latest_version="" - if [ -n "$latest_version" ]; then - printf "โ†’ $latest_version (UPDATE AVAILABLE)\n" - # Store: original_spec|package_name|new_version - echo "$package_spec|$package_name|$latest_version" >> "$updates_file" + if [ -n "$latest_version" ] && [ "$latest_version" != "$current_version" ]; then + # Check if it's a major version bump + if is_major_version_bump "$current_version" "$latest_version"; then + printf "โ†’ $latest_version (MAJOR UPDATE AVAILABLE)\n" + # Store in major updates file: original_spec|package_name|new_version + echo "$package_spec|$package_name|$latest_version" >> "$major_updates_file" + else + printf "โ†’ $latest_version (UPDATE AVAILABLE)\n" + # Store in regular updates file + echo "$package_spec|$package_name|$latest_version" >> "$updates_file" + fi else printf "โœ“ up to date\n" fi @@ -177,21 +203,43 @@ check_and_update_deps() { echo local update_count=0 + local major_update_count=0 if [ -f "$updates_file" ] && [ -s "$updates_file" ]; then update_count=$(wc -l < "$updates_file") fi - info "Updates available: $update_count" + if [ -f "$major_updates_file" ] && [ -s "$major_updates_file" ]; then + major_update_count=$(wc -l < "$major_updates_file") + fi + + local total_updates=$((update_count + major_update_count)) + info "Updates available: $total_updates" + if [ "$major_update_count" -gt 0 ]; then + echo -e "${RED}โš ๏ธ Major version updates detected: $major_update_count${RESET}" + fi - if [ "$update_count" -eq 0 ]; then + if [ "$total_updates" -eq 0 ]; then success "All packages are up to date!" return 0 fi - # Show updates available - info "Updates found:" - while IFS='|' read -r orig_spec pkg_name new_version; do - info " $pkg_name โ†’ $new_version" - done < "$updates_file" + # Show major updates with warning + if [ "$major_update_count" -gt 0 ]; then + echo + echo -e "${RED}โš ๏ธ MAJOR VERSION UPDATES (Breaking changes possible):${RESET}" + while IFS='|' read -r orig_spec pkg_name new_version; do + local current=$(get_current_version_from_spec "$orig_spec") + echo -e "${RED} $pkg_name: $current โ†’ $new_version${RESET}" + done < "$major_updates_file" + fi + + # Show regular updates + if [ "$update_count" -gt 0 ]; then + echo + info "Minor/Patch updates:" + while IFS='|' read -r orig_spec pkg_name new_version; do + info " $pkg_name โ†’ $new_version" + done < "$updates_file" + fi echo if [ "$auto_update" = false ]; then @@ -199,31 +247,66 @@ check_and_update_deps() { return 0 fi - # Ask user if they want to update (unless --yes flag is used) - if [ "$force_yes" = false ]; then - read -p "Do you want to update pyproject.toml with these versions? (y/N): " -r response - response=${response,,} # Convert to lowercase + # Handle major version updates separately + local approved_major_updates_file=$(mktemp) + if [ "$major_update_count" -gt 0 ]; then + echo + echo -e "${YELLOW}Review MAJOR version updates carefully (may contain breaking changes):${RESET}" + while IFS='|' read -r orig_spec pkg_name new_version <&3; do + local current=$(get_current_version_from_spec "$orig_spec") + read -p "Update $pkg_name from $current to $new_version? (y/N): " -r response + response=${response,,} + if [[ "$response" =~ ^(y|yes)$ ]]; then + echo "$orig_spec|$pkg_name|$new_version" >> "$approved_major_updates_file" + info "โœ“ Approved $pkg_name update" + else + info "โœ— Skipped $pkg_name update" + fi + done 3< "$major_updates_file" + echo + fi + + # Ask user about regular updates + if [ "$update_count" -gt 0 ]; then + read -p "Update $update_count minor/patch packages? (y/N): " -r response + response=${response,,} if [[ ! "$response" =~ ^(y|yes)$ ]]; then info "Update cancelled." + rm -f "$approved_major_updates_file" "$updates_file" "$major_updates_file" return 0 fi - else - info "Auto-updating due to --yes flag..." fi + # Combine approved updates + local final_updates=$(mktemp) + [ -f "$approved_major_updates_file" ] && [ -s "$approved_major_updates_file" ] && cat "$approved_major_updates_file" >> "$final_updates" + [ -f "$updates_file" ] && [ -s "$updates_file" ] && cat "$updates_file" >> "$final_updates" + + if [ ! -s "$final_updates" ]; then + info "No updates to apply." + rm -f "$approved_major_updates_file" "$updates_file" "$major_updates_file" "$final_updates" + return 0 + fi + + # Count approved updates + local approved_count=$(wc -l < "$final_updates") + step "Applying $approved_count package update(s) to pyproject.toml..." + # Update pyproject.toml local updated_content updated_content=$(cat "$PYPROJECT") + local applied_count=0 while IFS='|' read -r orig_spec pkg_name new_version; do # Escape special regex characters in the original spec local escaped_spec=$(echo "$orig_spec" | sed 's/[[\.*^$/]/\\&/g') # Replace the old spec with package name pinned to new version (==) updated_content=$(echo "$updated_content" | sed "s/\"${escaped_spec}\"/\"${pkg_name}==${new_version}\"/g") - done < "$updates_file" + applied_count=$((applied_count + 1)) + done < "$final_updates" echo "$updated_content" > "$PYPROJECT" - success "Updated $update_count packages in pyproject.toml" + success "Updated $applied_count packages in pyproject.toml" # Run uv lock to update the lock file step "Updating uv.lock..." @@ -232,6 +315,8 @@ check_and_update_deps() { echo info "Done! You may want to run 'uv sync' to install the updated packages." + + rm -f "$approved_major_updates_file" "$updates_file" "$major_updates_file" "$final_updates" } # ============================================================================= @@ -239,7 +324,7 @@ check_and_update_deps() { # ============================================================================= if [ "$CHECK_UPDATES" = true ]; then - check_and_update_deps "$AUTO_UPDATE" "$UPDATE_YES" + check_and_update_deps "$AUTO_UPDATE" exit 0 fi From 6e407026b04c200fdaed433350ce6d86cd3cc798 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 14 Jan 2026 11:56:01 +0100 Subject: [PATCH 092/199] Refactor dependency definitions to use 'dependency-groups' and 'dev-dependencies' for improved clarity and organization --- pyproject.toml | 2 +- uv.lock | 32 ++++++++++++++++++-------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9c9d8a77..2558f701 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ # Optional dependency groups for different environments and use cases. # These are used by pip-compile with --extra flags to generate different requirement files. -[project.optional-dependencies] +[dependency-groups] # Development and testing dependencies. # Includes code quality tools, testing frameworks, and development utilities. # Compiled into requirements-dev.txt (core + extra + dev). diff --git a/uv.lock b/uv.lock index b96fba89..e8a22fe7 100644 --- a/uv.lock +++ b/uv.lock @@ -899,7 +899,7 @@ dependencies = [ { name = "werkzeug" }, ] -[package.optional-dependencies] +[package.dev-dependencies] dev = [ { name = "beartype" }, { name = "black" }, @@ -917,25 +917,29 @@ dev = [ [package.metadata] requires-dist = [ - { name = "beartype", marker = "extra == 'dev'", specifier = "==0.22.9" }, - { name = "black", marker = "extra == 'dev'", specifier = "==25.12.0" }, - { name = "detect-secrets", marker = "extra == 'dev'", specifier = "==1.5.0" }, { name = "filelock", specifier = "==3.20.3" }, { name = "flask", specifier = "==3.1.2" }, - { name = "mypy", marker = "extra == 'dev'", specifier = "==1.19.1" }, - { name = "nbstripout", marker = "extra == 'dev'", specifier = "==0.8.2" }, - { name = "pip-audit", marker = "extra == 'dev'", specifier = "==2.10.0" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = "==4.5.1" }, - { name = "pytest", marker = "extra == 'dev'", specifier = "==9.0.2" }, - { name = "pytest-beartype", marker = "extra == 'dev'", specifier = "==0.2.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = "==7.0.0" }, { name = "requests", specifier = "==2.32.5" }, - { name = "rich", marker = "extra == 'dev'", specifier = "==14.2.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = "==0.14.11" }, { name = "urllib3", specifier = "==2.6.3" }, { name = "werkzeug", specifier = "==3.1.5" }, ] -provides-extras = ["dev", "extra"] + +[package.metadata.requires-dev] +dev = [ + { name = "beartype", specifier = "==0.22.9" }, + { name = "black", specifier = "==25.12.0" }, + { name = "detect-secrets", specifier = "==1.5.0" }, + { name = "mypy", specifier = "==1.19.1" }, + { name = "nbstripout", specifier = "==0.8.2" }, + { name = "pip-audit", specifier = "==2.10.0" }, + { name = "pre-commit", specifier = "==4.5.1" }, + { name = "pytest", specifier = "==9.0.2" }, + { name = "pytest-beartype", specifier = "==0.2.0" }, + { name = "pytest-cov", specifier = "==7.0.0" }, + { name = "rich", specifier = "==14.2.0" }, + { name = "ruff", specifier = "==0.14.11" }, +] +extra = [] [[package]] name = "pytokens" From 44f17fe716bc02d846f956600fdf9cee078e9f46 Mon Sep 17 00:00:00 2001 From: Dominic Istha Date: Fri, 27 Feb 2026 09:22:56 +0100 Subject: [PATCH 093/199] Adds CSV and Excel files to gitignore --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index 6e9760d1..6cd56d82 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,14 @@ venv/ env/ ENV/ +# CSV and Excel files +*.csv +*.xlsx +*.xls +*.xlsm +*.xlsb +*.ods + # Jupyter Notebook checkpoints .ipynb_checkpoints/ From 78f757226107dfb4367f99200be5712dcc902dd3 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 14:28:27 +0100 Subject: [PATCH 094/199] Refactor dependency management to use uv and update version specifications - Updated dependencies in pyproject.toml to use version ranges for better compatibility. - Removed the legacy dependency management script (dependency.sh) in favor of uv. - Simplified setup.sh to integrate uv for locking and syncing dependencies. - Adjusted setup process to verify and install uv if not present. - Updated uv.lock to reflect new version specifications. --- .pre-commit-config.yaml | 98 +---- .pre-commit-windows.yaml | 70 ---- .secrets.baseline | 2 +- README.md | 54 ++- docs/0_setup.md | 11 +- docs/1_usage.md | 24 +- docs/3_dependency_management.md | 620 +++++++++++++++++++++++--------- pyproject.toml | 56 +-- scripts/dependency.sh | 402 --------------------- scripts/setup.sh | 139 ++----- uv.lock | 34 +- 11 files changed, 597 insertions(+), 913 deletions(-) delete mode 100644 .pre-commit-windows.yaml delete mode 100755 scripts/dependency.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81fde4bc..059fb8a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,45 +1,18 @@ repos: - repo: local hooks: - # Verify requirements lockfiles - - id: verify-prod-lockfile - name: Verify production lockfile is up-to-date - entry: ./scripts/hooks/verify-lockfile.sh - args: ["requirements.txt"] - language: python - files: ^requirements\.txt$ - additional_dependencies: [pip-tools==7.5.0] - - - id: verify-dev-lockfile - name: Verify development lockfile is up-to-date - entry: ./scripts/hooks/verify-lockfile.sh - args: ["requirements-dev.txt"] - language: python - files: ^requirements-dev\.txt$ - additional_dependencies: [pip-tools==7.5.0] - - - id: verify-full-lockfile - name: Verify extra lockfile is up-to-date - entry: ./scripts/hooks/verify-lockfile.sh - args: ["requirements-full.txt"] - language: python - files: ^requirements-full\.txt$ - additional_dependencies: [pip-tools==7.5.0] - # Strip notebook outputs - id: nbstripout-autoadd name: nbstripout (auto-add notebooks) - entry: scripts/hooks/nbstripout-autoadd.sh - language: python + entry: uv run scripts/hooks/nbstripout-autoadd.sh + language: system types: [jupyter] - additional_dependencies: [nbstripout] # Auto-format Python code with Black - id: black-autoformat name: Auto-format Python with Black - entry: scripts/hooks/black-autoformat.sh - language: python - additional_dependencies: [black] + entry: uv run scripts/hooks/black-autoformat.sh + language: system types: [python] - repo: https://github.com/pre-commit/pre-commit-hooks @@ -57,12 +30,12 @@ repos: args: ["--autofix"] exclude: \.ipynb$ - # Dependency vulnerability scanning + # Dependency vulnerability scanning with uv.lock - repo: https://github.com/pypa/pip-audit - rev: v2.9.0 + rev: v2.10.0 hooks: - id: pip-audit - args: ["--progress-spinner", "off", "-r", "requirements.txt"] + args: ["--progress-spinner", "off", "--desc", "--ignore", "CVE-2026-1703"] stages: [pre-commit] # Secrets scanning @@ -73,75 +46,38 @@ repos: args: ["--baseline", ".secrets.baseline"] exclude: ^tests/ - # Notebook/text sync + # Notebook/text sync with jupytext - repo: https://github.com/mwouts/jupytext - rev: v1.18.0rc0 + rev: v1.19.1 hooks: - id: jupytext args: [--sync] require_serial: true + # Type checking with mypy - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.1 hooks: - id: mypy name: Type check with mypy args: ["--config-file=pyproject.toml"] - + # Linting and formatting with ruff - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.0 + rev: v0.15.4 hooks: - id: ruff - name: Ruff (linting + extra isort + rules) + name: Ruff (linting + import sorting + formatting) args: [--fix] types: [python] - # - repo: local - # hooks: - # - id: pytest - # name: Run pytest with coverage & beartype - # entry: pytest --cov --cov-fail-under=80 --beartype-packages=utils,example - # language: python - # additional_dependencies: - # - pytest - # - pytest-cov - # - pytest-beartype - # pass_filenames: false - # always_run: true - # stages: [pre-commit] - + # Testing with pytest (locally managed by uv) - repo: local hooks: - id: pytest - name: Run pytest with coverage & beartype - entry: bash scripts/hooks/precommit_pytest.sh + name: Run pytest with coverage + entry: uv run pytest --cov --cov-fail-under=80 --beartype-packages=utils,example language: system pass_filenames: false always_run: true stages: [pre-commit] - - # Frontend hooks (uncomment if you have a frontend in the `web/` directory) - # - id: frontend-lint - # name: Frontend lint - # entry: bash -c "cd web && npm run lint -- --fix" - # language: system - # files: ^web/ - # pass_filenames: false - # additional_dependencies: [] - - # - id: frontend-typecheck - # name: Frontend typecheck - # entry: bash -c "cd web && npm run typecheck" - # language: system - # files: ^web/ - # pass_filenames: false - # additional_dependencies: [] - - # - id: frontend-tests - # name: Frontend tests - # entry: bash -c "cd web && npm run test:run" - # language: system - # files: ^web/ - # pass_filenames: false - # additional_dependencies: [] diff --git a/.pre-commit-windows.yaml b/.pre-commit-windows.yaml deleted file mode 100644 index 4e324437..00000000 --- a/.pre-commit-windows.yaml +++ /dev/null @@ -1,70 +0,0 @@ -repos: - - repo: local - hooks: - - id: verify-prod-lockfile - name: Verify prod lockfile (Windows) - entry: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\hooks\\verify-prod-lockfile.ps1 - language: system - pass_filenames: false - stages: [pre-commit] - - - id: verify-dev-lockfile - name: Verify dev lockfile (Windows) - entry: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\hooks\\verify-dev-lockfile.ps1 - language: system - pass_filenames: false - stages: [pre-commit] - - - repo: https://github.com/kynan/nbstripout - rev: 0.8.1 - hooks: - - id: nbstripout - - - repo: https://github.com/psf/black - rev: 25.1.0 - hooks: - - id: black - - - repo: https://github.com/pypa/pip-audit - rev: v2.9.0 - hooks: - - id: pip-audit - args: ['--progress-spinner', 'off'] - stages: [pre-commit] - - - repo: https://github.com/Yelp/detect-secrets - rev: v1.5.0 - hooks: - - id: detect-secrets - args: ['--baseline', '.secrets.baseline'] - exclude: ^tests/ - - - repo: https://github.com/mwouts/jupytext - rev: v1.17.3 - hooks: - - id: jupytext - args: [--sync] - require_serial: true - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 - hooks: - - id: mypy - args: ["--config-file=pyproject.toml"] - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.12 - hooks: - - id: ruff - args: [--fix] - types: [python] - - - repo: local - hooks: - - id: pytest - name: Run pytest with coverage & beartype - entry: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\hooks\\precommit_pytest.ps1 - language: system - pass_filenames: false - always_run: true - stages: [pre-commit] diff --git a/.secrets.baseline b/.secrets.baseline index ee168433..da31ed89 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -146,5 +146,5 @@ } ] }, - "generated_at": "2025-10-15T13:02:47Z" + "generated_at": "2026-03-03T13:21:38Z" } diff --git a/README.md b/README.md index 5df27fdb..11505833 100644 --- a/README.md +++ b/README.md @@ -196,16 +196,16 @@ This project uses **uv** for fast, reliable dependency management. uv provides a # Step 1: Make sure uv is installed (see "Installing uv" section above) uv --version -# Step 2: Setup environment and lock dependencies -./scripts/dependency.sh +# Step 2: Setup environment and dependencies +./scripts/setup.sh ``` -The `dependency.sh` script will: +The `setup.sh` script will: 1. โœ… Create a virtual environment (`.venv`) 2. โœ… Lock all dependencies into `uv.lock` 3. โœ… Sync and install everything -**Important:** With uv, you don't need to activate the virtual environment! Instead: +**Important:** You don't need to manually activate the virtual environment for most uv commands: - Use `uv run python script.py` to run Python scripts - Use `uv run pytest` to run tests - Use `uv run mypy .` to run type checking @@ -220,23 +220,21 @@ source .venv/bin/activate # Linux/macOS ```bash # First time: setup environment and lock dependencies -./scripts/dependency.sh +./scripts/setup.sh -# Day-to-day: install locked dependencies +# Day-to-day: sync dependencies (install/update from uv.lock) uv sync # Add a new package -# 1. Edit pyproject.toml and add the package +# 1. Edit pyproject.toml and add the package with version constraints # 2. Run: uv lock && uv sync -# Check for updates (without installing) -./scripts/dependency.sh --check - -# Update packages (interactive prompt) -./scripts/dependency.sh --update +# Update dependencies (updates uv.lock with newer compatible versions) +# For patch + minor updates (safe): +uv lock -# Update packages (auto-confirm all) -./scripts/dependency.sh --update -y +# For major version updates (breaking changes possible): +uv lock --upgrade ``` ### Dependency Layers @@ -287,24 +285,21 @@ uv sync uv sync --no-dev ``` -**Check for outdated packages:** +**Update dependencies (patch + minor versions only):** ```bash -./scripts/dependency.sh --check -``` - -**Update packages (interactive - asks for confirmation):** -```bash -./scripts/dependency.sh --update +uv lock +uv sync ``` -**Update packages automatically (no prompts):** +**Update dependencies (allow major version updates):** ```bash -./scripts/dependency.sh --update -y +uv lock --upgrade +uv sync ``` **Update a specific package:** ```bash -# Edit pyproject.toml to the version you want, then: +# Edit pyproject.toml to the version constraint you want, then: uv lock uv sync ``` @@ -320,11 +315,12 @@ The `uv.lock` file contains: ### Benefits Over pip-tools -- โšก **Much faster** dependency resolution -- ๐Ÿ“ **Simpler lock format** - one file, human-readable -- ๐Ÿ”„ **Unified workflow** - one tool for all dependency tasks +- โšก **Much faster** dependency resolution (10-100x faster) +- ๐Ÿ“ **Simpler lock format** - one file (`uv.lock`), human-readable +- ๐Ÿ”„ **Unified workflow** - one tool (`uv`) for all dependency tasks - ๐Ÿ›ก๏ธ **Better error messages** - clearer dependency conflict reporting - ๐Ÿš€ **Modern Python packaging** - supports PEP 508, PEP 517, PEP 660 +- ๐Ÿ”’ **Built-in security** - integrated vulnerability scanning with `uv pip audit` **๐Ÿ“– Detailed guide**: [docs/3_dependency_management.md](docs/3_dependency_management.md) @@ -392,9 +388,7 @@ pytest tests/test_specific.py | File | Purpose | |------|---------| | `pyproject.toml` | Project metadata, all dependencies, tool config | -| `requirements-slim.txt` | Compiled core dependencies only (auto-generated) | -| `requirements.txt` | Compiled production dependencies: core + extra (auto-generated) | -| `requirements-dev.txt` | Compiled all dependencies: core + extra + dev (auto-generated) | +| `uv.lock` | Locked dependency versions (auto-generated by uv) | | `.pre-commit-config.yaml` | Pre-commit hooks configuration | | `.envrc` | Environment variables (direnv) | | `.secrets.baseline` | Secret detection baseline | diff --git a/docs/0_setup.md b/docs/0_setup.md index ecae8d61..38d338e8 100644 --- a/docs/0_setup.md +++ b/docs/0_setup.md @@ -59,11 +59,12 @@ After the script finishes, **restart your terminal or PowerShell session** to en The setup script is designed to create a consistent and reproducible development environment by automating the following steps: 1. **System Checks**: Verifies that Python, Git, and other required tools are installed. -2. **Virtual Environment**: Creates an isolated Python virtual environment in a `venv/` directory. -3. **Dependency Installation**: Installs all production and development dependencies using `pip-tools` for pinned, reproducible builds. -4. **Pre-Commit Hooks**: Sets up automated code quality checks that run before every commit. -5. **Secret Management**: Initializes `detect-secrets` to prevent accidental credential exposure and prepares for 1Password integration. -6. **Environment Variables**: Configures `direnv` (on Linux/macOS) to automatically manage environment variables when you enter the project directory. +2. **Virtual Environment**: Creates an isolated Python virtual environment in a `.venv/` directory. +3. **Dependency Installation**: Installs all production and development dependencies using `uv` for fast, reproducible builds. +4. **Lock File Generation**: Creates `uv.lock` to ensure consistent dependency versions across all team members and CI/CD. +5. **Pre-Commit Hooks**: Sets up automated code quality checks that run before every commit. +6. **Secret Management**: Initializes `detect-secrets` to prevent accidental credential exposure and prepares for 1Password integration. +7. **Environment Variables**: Configures `direnv` (on Linux/macOS) to automatically manage environment variables when you enter the project directory. ## ๐Ÿ›Ÿ Common Setup Issues diff --git a/docs/1_usage.md b/docs/1_usage.md index e25eb829..7fcbd51d 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -7,10 +7,10 @@ This guide covers the day-to-day development workflow, including dependency mana 1. **Activate Environment**: Navigate to the project directory. If you configured `direnv` during setup, the environment will activate automatically. Otherwise, activate it manually: ```bash # Linux / macOS - source venv/bin/activate + source .venv/bin/activate # Windows - .venv/Scripts/Activate.ps1 + .venv\Scripts\Activate.ps1 ``` 2. **Code**: Create or modify your Python modules, notebooks, and documentation. @@ -43,6 +43,26 @@ This guide covers the day-to-day development workflow, including dependency mana ``` At this point, the pre-commit hooks will run automatically. See the troubleshooting guide below if any checks fail. +### ๐Ÿ”„ Adding or Updating Dependencies + +When you need to add or update dependencies, follow this workflow: + +```bash +# 1. Edit pyproject.toml and add/update your dependency with version constraints +# Example: "requests>=2.28.0,<3.0.0" + +# 2. Update the lock file (safe update - patch + minor versions only) +uv lock + +# 3. Or upgrade to major versions if breaking changes are acceptable +uv lock --upgrade + +# 4. Sync your environment with the updated dependencies +uv sync +``` + +Always use version constraints like `>=X.Y.Z,=3.1.0,<4.0.0", + "requests>=2.32.0,<3.0.0", + "sqlalchemy>=2.0.0,<3.0.0", +] + +# Optional dependency groups (managed with --group flags) +[dependency-groups] + +# Additional production features (optional) +extra = [ + "redis>=4.0.0,<5.0.0", + "celery>=5.2.0,<6.0.0", +] + +# Development tools (development only) +dev = [ + "pytest>=9.0.0,<10.0.0", + "black>=25.12.0,<26.0.0", + "mypy>=1.19.0,<2.0.0", + "ruff>=0.14.0,<1.0.0", +] +``` This layered approach ensures: -- **Minimal deployments** with only essential dependencies -- **Flexible production** setups with optional feature control -- **Complete development** environments with all tooling -- **Reproducible builds** with fully pinned, hashed lockfiles - **Clear separation** between runtime, optional, and tooling packages +- **Flexible deployments** with control over which packages are installed +- **Reproducible builds** with single-file lockfile (`uv.lock`) +- **Fast dependency resolution** with uv's optimized algorithm +- **Version safety** with pinned versions in the lock file + +### Installation Matrix + +| Scenario | Command | Core | Extra | Dev | Use Case | +|----------|---------|------|-------|-----|----------| +| Development (all groups) | `uv sync` | โœ… | โœ… | โœ… | Full environment for coding | +| Production (core + extra) | `uv sync --no-group dev` | โœ… | โœ… | โŒ | Production servers | +| Production minimal (core only) | `uv sync --no-group dev --no-group extra` | โœ… | โŒ | โŒ | Minimal deployments | +| Development without extra | `uv sync --no-group extra` | โœ… | โŒ | โœ… | Local development, no optional features | +| Only extra (unusual) | `uv sync --only-group extra` | โŒ | โœ… | โŒ | Feature testing only | +| Only dev (testing) | `uv sync --only-group dev` | โŒ | โŒ | โœ… | CI/test environments without core | --- -## 3.2 Using the helper script (`./scripts/dependency.sh`) +## 3.2 Understanding Dependency Groups (PEP 735) -The provided script automates compilation and installation across all dependency layers. +This project uses **PEP 735 format** with `[dependency-groups]` for modern dependency management. This is the standard supported by uv and enables flexible grouping of optional dependencies. -```bash -# Install all dependencies (core + extra + dev) - Default for development -./scripts/dependency.sh +### What Are Dependency Groups? + +Dependency groups are named collections of packages in the `[dependency-groups]` section. Unlike the older `[project.optional-dependencies]` format, PEP 735 groups are: +- **Easier to manage** - Clear syntax in a single section +- **Better supported** - Native support in modern tools like uv +- **More flexible** - Arbitrary naming and multiple groups +- **Lockfile-aware** - All groups locked together in `uv.lock` + +### Pre-defined Groups in This Project -# Install core dependencies only - Minimal deployment -./scripts/dependency.sh --slim +| Group | Purpose | Default Included | When to Use | +|-------|---------|------------------|------------| +| `extra` | Optional production features | โœ… in dev, โŒ in prod | Feature flags, optional integrations | +| `dev` | Development and testing tools | โœ… in dev mode | Local development, CI/CD | + +### Adding Custom Groups + +You can add additional groups as needed: + +```toml +[dependency-groups] -# Install production dependencies (core + extra) - Full production deployment -./scripts/dependency.sh --prod +# Standard groups +extra = ["redis>=4.0.0,<5.0.0"] +dev = ["pytest>=9.0.0,<10.0.0"] -# Install development dependencies only - CI testing environments -./scripts/dependency.sh --dev +# Custom groups (examples) +docs = [ # Documentation generation + "sphinx>=7.0.0,<8.0.0", + "sphinx-rtd-theme>=2.0.0,<3.0.0", +] + +test = [ # Just testing, minimal tooling + "pytest>=9.0.0,<10.0.0", + "pytest-cov>=7.0.0,<8.0.0", +] + +perf = [ # Performance optimization tools + "py-spy>=0.3.14,<1.0.0", + "memory-profiler>=0.61.0,<1.0.0", +] ``` -### Installation Matrix +Then install with: +```bash +uv sync --group docs # Include docs group with core +uv sync --group perf --group test # Multiple custom groups +uv sync --only-group docs # Only docs (advanced) +``` -| Command | Core | Extra | Dev | Use Case | -|---------|------|-------|-----|----------| -| `--slim` | โœ… | โŒ | โŒ | Containers, Lambda, minimal deployments | -| `--prod` | โœ… | โœ… | โŒ | Production servers with all features | -| `--dev` | โŒ | โŒ | โœ… | CI environments, testing only | -| (default) | โœ… | โœ… | โœ… | Local development, complete setup | +--- -### 3.2.1 Under the hood (manual steps) +## 3.3 Version Constraints and Pinning -The script follows a two-phase approach: **compile all lockfiles first**, then **install selected dependencies**. +All dependencies use **semantic versioning** with flexible constraints: -#### Phase 1: Compile All Lockfiles +```toml +# Format: >=CURRENT.Y.Z,=3.1.0,<4.0.0", # Allows 3.1.x, 3.2.x, 3.3.x, etc. + "requests>=2.32.0,<3.0.0", # Allows 2.32.x, 2.33.x, etc. + "sqlalchemy>=2.0.0,<3.0.0", # Allows 2.0.x, 2.1.x, etc. +] +``` -1. Activates the project virtual environment (`.venv/bin/activate`). -2. Installs or verifies **pip-tools**. -3. Compiles the **slim lockfile** (core dependencies only): +### Version Constraint Strategy - ```bash - pip-compile pyproject.toml --output-file=requirements-slim.txt --generate-hashes --strip-extras - ``` +- **`>=X.Y.Z`**: Minimum compatible version (includes patches and minor updates) +- **`=3.1.0,<4.0.0` | Explicit version range for reproducibility | +| `"flask==3.1.2"` | `flask>=3.1.0,<4.0.0` | Allows patches without manual updates | +| `"flask>=3.1.0"` | `flask>=3.1.0,<4.0.0` | Prevents breaking major updates | +| `"flask>=2.0.0"` | `flask>=3.1.0,<4.0.0` | Too loose, allows old versions | -5. Compiles the **development lockfile** (core + extra + dev): +--- - ```bash - pip-compile pyproject.toml --extra=dev --extra=extra --output-file=requirements-dev.txt --generate-hashes --strip-extras - ``` +## 3.4 Working with uv -#### Phase 2: Install Selected Dependencies +### First-Time Setup -6. Installs packages based on the selected option: - - `--slim`: `pip install --require-hashes -r requirements-slim.txt` - - `--prod`: `pip install -r requirements.txt` - - `--dev`: `pip install -r requirements-dev.txt` - - (default): `pip install -r requirements-dev.txt` +```bash +# Install uv if you haven't already +curl -LsSf https://astral.sh/uv/install.sh | sh -7. Runs `pip check` to validate compatibility. +# Run the setup script - creates environment and locks dependencies +./scripts/setup.sh +``` + +This creates: +- `.venv/` - Isolated Python environment +- `uv.lock` - Locked versions of all dependencies -#### Why Two Phases? +### Day-to-Day Workflow -This approach ensures **lockfile consistency** before installation, preventing errors from manually edited or corrupted requirement files. +```bash +# Syncing existing dependencies (most common operation) +# Downloads and installs packages according to uv.lock +uv sync + +# Adding a new dependency +# 1. Edit pyproject.toml and add the package with version constraints +# 2. Update the lock file +uv lock +# 3. Install the new packages +uv sync +``` -### 3.2.2 Hash Requirements and Security +### Update Workflows + +#### Safe Updates (patch + minor versions) + +```bash +# Updates uv.lock with newest compatible patch/minor versions +# Example: 3.1.2 โ†’ 3.2.5 (but not 4.0.0) +uv lock +uv sync +``` -By default, the script uses **hash-based verification** for maximum security: +#### Major Version Updates -- **`--generate-hashes`** during compilation creates SHA256 hashes for each dependency -- **`--require-hashes`** during installation verifies packages against these hashes +```bash +# Updates uv.lock allowing major version changes +# Example: 3.x.x โ†’ 4.0.0 (may have breaking changes!) +# Use this carefully and test thoroughly +uv lock --upgrade +uv sync +``` -This prevents **supply chain attacks** by ensuring installed packages match exactly what was specified. +#### Updating a Specific Package -#### When to Disable Hash Verification +```bash +# Edit pyproject.toml: +# Change: "requests>=2.32.0,<3.0.0" +# To: "requests>=2.35.0,<3.0.0" -Some packages **cannot be hashed** due to: -- Private packages from git repositories -- Local development packages (`file://` URLs) -- Packages with dynamic content that changes between installs +# Then lock and sync +uv lock +uv sync +``` -**For these cases, you can disable hash verification:** +#### Installing Additional Groups -1. **In `scripts/dependency.sh`**, modify the relevant `pip install` commands: - - Remove `--require-hashes` from the install command - - Example: Change `pip install --require-hashes -r requirements-slim.txt` to `pip install -r requirements-slim.txt` +If `extra` dependencies are optional: -2. **In `scripts/dependency.sh`**, modify the corresponding `pip-compile` commands: - - Remove `--generate-hashes` from the compile command - - Example: Change `pip-compile pyproject.toml --output-file=requirements-slim.txt --generate-hashes --strip-extras` to `pip-compile pyproject.toml --output-file=requirements-slim.txt --strip-extras` +```bash +# Install core + extra but not dev +uv sync --extra extra -**โš ๏ธ Security Warning:** Only disable hashes for packages that genuinely cannot be hashed. Keep hash verification enabled wherever possible to maintain supply chain security. +# Install specific groups +uv sync --group dev --group extra +``` --- -## 3.3 Best Practices and Rules - -### Dependency Classification Guidelines - -**Core Dependencies** (`dependencies`): -- Database drivers (psycopg2, pymongo) -- Web frameworks (Flask, FastAPI) -- HTTP clients (requests, httpx) -- Essential business logic libraries -- Authentication libraries - -**Extra Dependencies** (`[project.optional-dependencies].extra`): -- Caching clients (redis, memcached) -- Message queues (celery, rq) -- Monitoring tools (sentry-sdk, prometheus-client) -- Performance libraries (uvloop, orjson) -- Private/internal packages - -**Dev Dependencies** (`[project.optional-dependencies].dev`): -- Testing frameworks (pytest, coverage) -- Code quality tools (black, ruff, mypy) -- Development utilities (pre-commit, rich) -- Documentation tools (sphinx, mkdocs) - -### Rules and Best Practices - -| โœ… Do This | โŒ Don't Do This | -| --- | --- | -| Declare all deps in `pyproject.toml` only | Scatter deps across multiple files | -| Use `./scripts/dependency.sh` or manual pip-compile | Run `pip install ` manually | -| Commit all three lockfiles (`*requirements*.txt`) | Edit lockfiles by hand | -| Choose the right dependency layer | Mix runtime and tooling dependencies | -| Use `--slim` for minimal deployments | Use full lockfile for containers | -| Regenerate lockfiles after changing `pyproject.toml` | Forget to update lockfiles | -| Run `pip check` after installation | Skip integrity verification | +## 3.5 The Lock File (`uv.lock`) + +The `uv.lock` file is the source of truth for reproducible builds. + +### What It Contains + +``` +# Human-readable format showing all transitive dependencies +# Each package with pinned version, hashes, and metadata +[[package]] +name = "requests" +version = "2.32.5" +requires-python = ">=3.7" + +[[package]] +name = "urllib3" +version = "2.6.3" +requires-python = ">=2.7,!=3.0.*,!=3.1.*" +``` + +### Best Practices + +- โœ… **Always commit `uv.lock`** to version control +- โœ… **Keep `uv.lock` in sync** with `pyproject.toml` +- โœ… **Review lock file changes** in pull requests for security updates +- โŒ **Never edit `uv.lock` manually** - always use `uv lock` command +- โŒ **Don't add git patterns** that exclude `uv.lock` + +### Why Commit the Lock File? + +The lock file ensures: +- All team members use identical dependency versions +- CI/CD deployments are reproducible +- No surprise dependency differences between environments +- Security issues can be tracked and resolved consistently --- -## 3.4 Adding New Dependencies +## 3.6 Adding New Dependencies + +### Step-by-Step Process -When adding packages, choose the appropriate section in `pyproject.toml`: +**1. Determine the right layer:** +- Is it needed for the app to run? โ†’ `dependencies` +- Is it optional production feature? โ†’ `[dependency-groups].extra` +- Is it development/testing tool? โ†’ `[dependency-groups].dev` +**2. Edit `pyproject.toml`:** + +For core dependencies: ```toml -# Core dependencies - Essential runtime requirements -# These go in ALL deployment scenarios dependencies = [ - "flask>=2.2.0,<3.0.0", # Web framework - "requests>=2.28.0", # HTTP client - "sqlalchemy>=2.0.0", # Database ORM - "pydantic>=2.0.0", # Data validation + # Existing dependencies... + "newvalidator>=1.5.0,<2.0.0", # Add with version constraint ] +``` + +For optional groups: +```toml +[dependency-groups] -[project.optional-dependencies] -# Extra dependencies - Optional production features -# These go in FULL production deployments only extra = [ - "redis>=4.0.0", # Caching - "celery>=5.2.0", # Task queue - "sentry-sdk>=1.12.0", # Error monitoring - "gunicorn>=20.1.0", # Production WSGI server + # Existing extra dependencies... + "redis>=4.0.0,<5.0.0", # Add new optional dependency ] -# Dev dependencies - Development and testing tools -# These go in DEVELOPMENT environments only dev = [ - "pytest>=7.0", # Testing framework - "black>=22.0", # Code formatter - "mypy>=1.0", # Type checker - "pre-commit>=2.20.0", # Git hooks + # Existing dev dependencies... + "pytest-mock>=3.10.0,<4.0.0", # Add new dev dependency ] ``` -### Decision Tree: Where to Put Dependencies +**3. Lock and sync:** + +```bash +uv lock # Updates uv.lock with the new package and its dependencies +uv sync # Installs the new package(s) +``` + +### Classification Decision Tree ``` -Is this package needed for the app to run? -โ”œโ”€ Yes: Is it absolutely essential? -โ”‚ โ”œโ”€ Yes โ†’ `dependencies` (core) -โ”‚ โ””โ”€ No โ†’ `[project.optional-dependencies].extra` -โ””โ”€ No: Is it for development/testing? - โ””โ”€ Yes โ†’ `[project.optional-dependencies].dev` +Is this package necessary for the app to function? +โ”œโ”€ Yes: Is it absolutely core? +โ”‚ โ”œโ”€ Yes โ†’ dependencies (core) +โ”‚ โ””โ”€ No โ†’ [dependency-groups].extra +โ””โ”€ No: Is it for development/testing/tooling? + โ””โ”€ Yes โ†’ [dependency-groups].dev ``` -## 3.5 Migration from Older Setups +### Examples + +**Core Dependencies** (needed for production): +- `flask` - Web framework +- `requests` - HTTP client +- `sqlalchemy` - ORM +- `pydantic` - Data validation +- `psycopg2-binary` - PostgreSQL driver + +**Extra Dependencies** (optional features): +- `redis` - Caching +- `celery` - Task queue +- `sentry-sdk` - Error monitoring +- `prometheus-client` - Metrics -### From Legacy `requirements.in` Workflow: +**Dev Dependencies** (development only): +- `pytest` - Testing +- `black` - Code formatter +- `mypy` - Type checker +- `ruff` - Linter +- `pre-commit` - Git hooks -1. **Classify dependencies** into core, extra, and dev categories -2. **Move essential runtime deps** into `[project.dependencies]` -3. **Move optional production deps** into `[project.optional-dependencies].extra` -4. **Move tooling dependencies** into `[project.optional-dependencies].dev` -5. **Remove** old `requirements.in` files -6. **Regenerate** all lockfiles: +--- - ```bash - ./scripts/dependency.sh - ``` +## 3.7 Common Workflows -### From Two-File Setup (requirements.txt + requirements-dev.txt): +### Checking for Updates -1. **Audit existing requirements.txt** - split into core vs extra -2. **Move accordingly** to `dependencies` and `[project.optional-dependencies].extra` -3. **Move dev requirements** to `[project.optional-dependencies].dev` -4. **Regenerate** with the helper script +```bash +# See what updates are available without installing +# (Review changes and test in a branch first) +uv lock --dry-run -## 3.6 Troubleshooting +# Or just check what uv would update +uv pip list --outdated +``` -### Common Issues +### Installing for Different Scenarios -**Dependency conflicts between layers:** ```bash -pip check # Identify conflicts -# Review and resolve version constraints in pyproject.toml +# Development setup (all groups - default) +uv sync + +# Production server (core + extra, no dev) +uv sync --no-group dev + +# Minimal production (core only, no extra, no dev) +uv sync --no-group dev --no-group extra + +# Development without optional features (core + dev, no extra) +uv sync --no-group extra + +# Only a specific group (advanced) +uv sync --only-group dev # dev only (no core dependencies!) +uv sync --only-group extra # extra only (no core dependencies!) + +# Multiple specific groups +uv sync --group dev --group extra # Combined with core automatically ``` -**Lockfile out of sync:** +### Understanding Group Flags + +| Flag | Behavior | +|------|----------| +| `uv sync` | Includes all: core + extra + dev | +| `--no-group dev` | Excludes: removes dev group | +| `--no-group extra` | Excludes: removes extra group | +| `--only-group dev` | Includes ONLY: dev group (โš ๏ธ no core!) | +| `--group extra` | Includes: core + this group | + +### Cleaning Up + ```bash -# Force regeneration of all lockfiles -./scripts/dependency.sh +# Remove virtual environment +rm -rf .venv + +# Recreate from scratch +uv sync ``` -**Package in wrong layer:** -- Check if a "dev" package is actually needed at runtime -- Move production packages from "dev" to "core" or "extra" -- Use the decision tree above to classify correctly +### Troubleshooting Dependency Conflicts + +```bash +# If you get dependency conflicts, inspect what's installed +pip list + +# Check if there are any conflicts +pip check + +# Review the conflict in pyproject.toml and adjust version constraints +# Then retry: +uv lock +uv sync +``` + +--- + +## 3.8 Migration from Requirements Files + +If you're coming from an older pip-tools setup with `requirements.txt` files: + +1. **Audit dependencies** - Identify which packages are core vs extra vs dev +2. **Update `pyproject.toml`** with proper layer assignments using **PEP 735**: + ```toml + # Core runtime (always installed) + dependencies = [ + "flask>=3.1.0,<4.0.0", + # ... more core packages + ] + + # Optional dependency groups + [dependency-groups] + + # Optional features (install with --group extra) + extra = [ + "redis>=4.0.0,<5.0.0", + # ... more extra packages + ] + + # Development tools (install by default, skip with --no-group dev) + dev = [ + "pytest>=9.0.0,<10.0.0", + # ... more dev packages + ] + ``` +3. **Lock everything** with `uv lock` +4. **Sync** with `uv sync` (or `uv sync --no-group dev` for production) +5. **Delete old requirement files** (they're no longer needed) + +--- + +## 3.9 Running Commands with uv + +You can run Python scripts and commands without explicitly activating the virtual environment: + +```bash +# Run a Python script +uv run python script.py + +# Run pytest +uv run pytest + +# Run mypy +uv run mypy . + +# Run black +uv run black . + +# Run ruff +uv run ruff check . --fix + +# Run any command with the virtual environment active +uv run +``` + +Or activate the environment manually: + +```bash +# Linux / macOS +source .venv/bin/activate + +# Windows +.venv\Scripts\Activate.ps1 + +# Then use tools normally +pytest +mypy . +black . +``` + +--- + +## 3.10 Best Practices Summary + +### Do โœ… + +- Declare all dependencies in `pyproject.toml` only +- Use semantic versioning: `>=X.Y.Z,= and < to allow patch/minor updates while preventing major version breaks. +# Add only dependencies that are needed for the app to run in production. dependencies = [ - "flask==3.1.2", # Example dependency, replace with your own - "requests==2.32.5", - "urllib3==2.6.3", - "werkzeug==3.1.5", - "filelock==3.20.3", + "flask>=3.1.0,<4.0.0", # Example dependency, replace with your own + "requests>=2.32.0,<3.0.0", + "urllib3>=2.6.0,<3.0.0", + "werkzeug>=3.1.0,<4.0.0", + "filelock>=3.20.0,<4.0.0", ] # Optional dependency groups for different environments and use cases. -# These are used by pip-compile with --extra flags to generate different requirement files. +# Managed by uv (PEP 735 format) and locked in uv.lock for reproducible builds. +# Install with: uv sync (all groups), uv sync --no-group dev (no dev), uv sync --only-group extra (extra only), etc. [dependency-groups] + +# Additional production features and optional components. +# Install with: uv sync --group extra or uv sync --no-group extra +extra = [ + # "zero-friction @ git+https://github.com/TECHLETES/zero_friction.git@main" +] + # Development and testing dependencies. # Includes code quality tools, testing frameworks, and development utilities. -# Compiled into requirements-dev.txt (core + extra + dev). +# Install with: uv sync (default includes dev) or uv sync --no-group dev to skip dev = [ - "pre-commit==4.5.1", - "detect-secrets==1.5.0", - "nbstripout==0.8.2", - "pytest==9.0.2", - "pytest-cov==7.0.0", - "black==25.12.0", - "mypy==1.19.1", - "ruff==0.14.11", - "beartype==0.22.9", - "pytest-beartype==0.2.0", - "pip-audit==2.10.0", - "rich==14.2.0", + "pre-commit>=4.5.0,<5.0.0", + "detect-secrets>=1.5.0,<2.0.0", + "nbstripout>=0.8.0,<1.0.0", + "pytest>=9.0.0,<10.0.0", + "pytest-cov>=7.0.0,<8.0.0", + "black>=25.12.0,<26.0.0", + "mypy>=1.19.0,<2.0.0", + "ruff>=0.14.0,<1.0.0", + "beartype>=0.22.0,<1.0.0", + "pytest-beartype>=0.2.0,<1.0.0", + "pip-audit>=2.10.0,<3.0.0", + "rich>=14.2.0,<15.0.0", ] -# Additional production dependencies. -# Includes private packages or optional features not in the core dependencies. -# Compiled into requirements.txt (core + extra). -extra = [ - # "zero-friction @ git+https://github.com/TECHLETES/zero_friction.git@main" -] [project.urls] Homepage = "https://github.com/TECHLETES/python_basic_template" diff --git a/scripts/dependency.sh b/scripts/dependency.sh deleted file mode 100755 index 24f64039..00000000 --- a/scripts/dependency.sh +++ /dev/null @@ -1,402 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ============================================================================= -# uv Dependency Management Script -# ============================================================================= -# A modern dependency management helper using uv for fast, reliable builds -# -# Usage: -# ./scripts/dependency.sh # Setup dev environment (default) -# ./scripts/dependency.sh --prod # Production environment -# ./scripts/dependency.sh --no-sync # Compile only, skip sync -# ./scripts/dependency.sh --check # Check for package updates -# ./scripts/dependency.sh --update # Check and auto-update packages -# -# This script replaces the previous pip-tools workflow with uv, which provides: -# - Faster dependency resolution -# - Simpler, more transparent lock files -# - Better compatibility with modern Python packaging -# -# ============================================================================= - -# Configuration -export VENV_DIR="${VENV_DIR:-.venv}" -export PYPROJECT="${PYPROJECT:-pyproject.toml}" -SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Colors for output -YELLOW='\033[1;33m' -GREEN='\033[1;32m' -BLUE='\033[0;34m' -RED='\033[0;31m' -RESET='\033[0m' - -# Helper functions -info() { echo -e "${BLUE}โ„น๏ธ $1${RESET}"; } -step() { echo -e "\n${YELLOW}$1${RESET}"; } -success() { echo -e "${GREEN}โœ… $1${RESET}"; } -error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } - -# Argument Parsing -PROD_MODE=false -SKIP_SYNC=false -CHECK_UPDATES=false -AUTO_UPDATE=false - -while [ $# -gt 0 ]; do - case "$1" in - --prod) - PROD_MODE=true - shift - ;; - --no-sync) - SKIP_SYNC=true - shift - ;; - --check) - CHECK_UPDATES=true - shift - ;; - --update) - AUTO_UPDATE=true - CHECK_UPDATES=true - shift - ;; - -h|--help) - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " --prod Production environment (no dev tools)" - echo " --no-sync Lock only, skip sync" - echo " --check Check for package updates" - echo " --update Check and update packages (interactive)" - echo " -h, --help Show this help message" - exit 0 - ;; - *) - error "Unknown option: $1. Use --help for usage information." - ;; - esac -done - -# Check if uv is installed -step "Checking for uv installation..." -if ! command -v uv &> /dev/null; then - error "uv is not installed. Please install it with: https://docs.astral.sh/uv/getting-started/installation/" -fi -success "uv is installed ($(uv --version))" - -# ============================================================================= -# HELPER FUNCTIONS FOR UPDATES -# ============================================================================= - -get_major_version() { - local version=$1 - echo "$version" | cut -d. -f1 -} - -get_current_version_from_spec() { - local spec=$1 - # Extract version from spec like "package>=1.2.3,<2.0.0" - # Gets the first version number found - echo "$spec" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+|[0-9]+\.[0-9]+' | head -1 -} - -is_major_version_bump() { - local current_version=$1 - local new_version=$2 - - if [ -z "$current_version" ]; then - return 1 # No current version, so not a bump - fi - - local current_major=$(get_major_version "$current_version") - local new_major=$(get_major_version "$new_version") - - [ "$current_major" != "$new_major" ] -} - -get_latest_version() { - local package=$1 - - # Fetch latest version from PyPI API - local json_response=$(curl -s "https://pypi.org/pypi/${package}/json" 2>/dev/null) - - if [ -z "$json_response" ]; then - return 1 - fi - - # Extract version from JSON (basic extraction, no jq dependency) - local version=$(echo "$json_response" | grep -oP '"version":\s*"\K[^"]+' | head -1) - - if [ -n "$version" ]; then - echo "$version" - return 0 - fi - return 1 -} - -parse_pyproject() { - # Extract full package specifications from dependencies sections only - # Matches entries like: "package", "package>=1.0", "package>=1.0,<2.0", etc. - # Only in 'dependencies' and 'dev' sections, skipping classifiers - awk ' - /^\[project\]$/ {in_deps=1; next} - /^\[project.optional-dependencies/ {in_deps=2; next} - /^\[/ && !/^\[project/ {in_deps=0; next} - (in_deps==1 || in_deps==2) && /^\s*"[a-zA-Z0-9_\-\.@]+/ && !/^dependencies|^dev/ { - # Skip lines that contain these patterns (from classifiers and config) - if ($0 !~ /::/) { - match($0, /"([^"]+)"/, arr) - if (arr[1]) print arr[1] - } - } - ' "$PYPROJECT" -} - -check_and_update_deps() { - local auto_update=$1 - - step "Checking for package updates..." - - if [ ! -f "$PYPROJECT" ]; then - error "pyproject.toml not found!" - fi - - # Parse current packages - local packages=$(parse_pyproject) - local pkg_count=$(echo "$packages" | wc -l) - info "Found $pkg_count pinned packages" - echo - - # Check for updates - use temp files instead of associative array for compatibility - local updates_file=$(mktemp) - local major_updates_file=$(mktemp) - trap "rm -f $updates_file $major_updates_file" RETURN - - while read -r package_spec; do - # Extract package name (everything before version operators) - local package_name=$(echo "$package_spec" | sed 's/[><=~!@].*//') - local current_version=$(get_current_version_from_spec "$package_spec") - - printf "Checking $package_name (current: $package_spec)... " - - local latest_version - latest_version=$(get_latest_version "$package_name" 2>/dev/null) || latest_version="" - - if [ -n "$latest_version" ] && [ "$latest_version" != "$current_version" ]; then - # Check if it's a major version bump - if is_major_version_bump "$current_version" "$latest_version"; then - printf "โ†’ $latest_version (MAJOR UPDATE AVAILABLE)\n" - # Store in major updates file: original_spec|package_name|new_version - echo "$package_spec|$package_name|$latest_version" >> "$major_updates_file" - else - printf "โ†’ $latest_version (UPDATE AVAILABLE)\n" - # Store in regular updates file - echo "$package_spec|$package_name|$latest_version" >> "$updates_file" - fi - else - printf "โœ“ up to date\n" - fi - done <<< "$packages" - - echo - local update_count=0 - local major_update_count=0 - if [ -f "$updates_file" ] && [ -s "$updates_file" ]; then - update_count=$(wc -l < "$updates_file") - fi - if [ -f "$major_updates_file" ] && [ -s "$major_updates_file" ]; then - major_update_count=$(wc -l < "$major_updates_file") - fi - - local total_updates=$((update_count + major_update_count)) - info "Updates available: $total_updates" - if [ "$major_update_count" -gt 0 ]; then - echo -e "${RED}โš ๏ธ Major version updates detected: $major_update_count${RESET}" - fi - - if [ "$total_updates" -eq 0 ]; then - success "All packages are up to date!" - return 0 - fi - - # Show major updates with warning - if [ "$major_update_count" -gt 0 ]; then - echo - echo -e "${RED}โš ๏ธ MAJOR VERSION UPDATES (Breaking changes possible):${RESET}" - while IFS='|' read -r orig_spec pkg_name new_version; do - local current=$(get_current_version_from_spec "$orig_spec") - echo -e "${RED} $pkg_name: $current โ†’ $new_version${RESET}" - done < "$major_updates_file" - fi - - # Show regular updates - if [ "$update_count" -gt 0 ]; then - echo - info "Minor/Patch updates:" - while IFS='|' read -r orig_spec pkg_name new_version; do - info " $pkg_name โ†’ $new_version" - done < "$updates_file" - fi - echo - - if [ "$auto_update" = false ]; then - info "To apply updates, run: ./scripts/dependency.sh --update" - return 0 - fi - - # Handle major version updates separately - local approved_major_updates_file=$(mktemp) - if [ "$major_update_count" -gt 0 ]; then - echo - echo -e "${YELLOW}Review MAJOR version updates carefully (may contain breaking changes):${RESET}" - while IFS='|' read -r orig_spec pkg_name new_version <&3; do - local current=$(get_current_version_from_spec "$orig_spec") - read -p "Update $pkg_name from $current to $new_version? (y/N): " -r response - response=${response,,} - if [[ "$response" =~ ^(y|yes)$ ]]; then - echo "$orig_spec|$pkg_name|$new_version" >> "$approved_major_updates_file" - info "โœ“ Approved $pkg_name update" - else - info "โœ— Skipped $pkg_name update" - fi - done 3< "$major_updates_file" - echo - fi - - # Ask user about regular updates - if [ "$update_count" -gt 0 ]; then - read -p "Update $update_count minor/patch packages? (y/N): " -r response - response=${response,,} - if [[ ! "$response" =~ ^(y|yes)$ ]]; then - info "Update cancelled." - rm -f "$approved_major_updates_file" "$updates_file" "$major_updates_file" - return 0 - fi - fi - - # Combine approved updates - local final_updates=$(mktemp) - [ -f "$approved_major_updates_file" ] && [ -s "$approved_major_updates_file" ] && cat "$approved_major_updates_file" >> "$final_updates" - [ -f "$updates_file" ] && [ -s "$updates_file" ] && cat "$updates_file" >> "$final_updates" - - if [ ! -s "$final_updates" ]; then - info "No updates to apply." - rm -f "$approved_major_updates_file" "$updates_file" "$major_updates_file" "$final_updates" - return 0 - fi - - # Count approved updates - local approved_count=$(wc -l < "$final_updates") - step "Applying $approved_count package update(s) to pyproject.toml..." - - # Update pyproject.toml - local updated_content - updated_content=$(cat "$PYPROJECT") - local applied_count=0 - - while IFS='|' read -r orig_spec pkg_name new_version; do - # Escape special regex characters in the original spec - local escaped_spec=$(echo "$orig_spec" | sed 's/[[\.*^$/]/\\&/g') - # Replace the old spec with package name pinned to new version (==) - updated_content=$(echo "$updated_content" | sed "s/\"${escaped_spec}\"/\"${pkg_name}==${new_version}\"/g") - applied_count=$((applied_count + 1)) - done < "$final_updates" - - echo "$updated_content" > "$PYPROJECT" - success "Updated $applied_count packages in pyproject.toml" - - # Run uv lock to update the lock file - step "Updating uv.lock..." - uv lock - success "Lock file updated" - - echo - info "Done! You may want to run 'uv sync' to install the updated packages." - - rm -f "$approved_major_updates_file" "$updates_file" "$major_updates_file" "$final_updates" -} - -# ============================================================================= -# EARLY EXIT FOR UPDATE OPERATIONS -# ============================================================================= - -if [ "$CHECK_UPDATES" = true ]; then - check_and_update_deps "$AUTO_UPDATE" - exit 0 -fi - -# Create or activate virtual environment -step "Setting up virtual environment at '${VENV_DIR}'..." -if [ ! -d "${VENV_DIR}" ]; then - info "Creating new virtual environment..." - uv venv "${VENV_DIR}" - success "Virtual environment created." -else - success "Virtual environment exists." -fi - -# Ensure the venv is valid -if [ ! -f "${VENV_DIR}/bin/activate" ] && [ ! -f "${VENV_DIR}/Scripts/activate" ]; then - error "Virtual environment is broken. Remove ${VENV_DIR} and rerun this script." -fi - -# ============================================================================= -# PHASE 1: LOCK DEPENDENCIES -# ============================================================================= -step "PHASE 1: Locking dependencies using uv..." - -if [ "$PROD_MODE" = true ]; then - info "Production mode: Locking production dependencies only (no dev tools)" - uv lock --no-dev - success "Production dependencies locked to uv.lock" -else - info "Development mode: Locking all dependencies (including dev tools)" - uv lock - success "All dependencies locked to uv.lock" -fi - -# ============================================================================= -# PHASE 2: SYNC ENVIRONMENT -# ============================================================================= -if [ "$SKIP_SYNC" = false ]; then - step "PHASE 2: Syncing environment with uv..." - - if [ "$PROD_MODE" = true ]; then - info "Installing production dependencies only (no dev packages)..." - uv sync --no-dev - success "Production environment synced." - else - info "Installing all dependencies (core + dev)..." - uv sync - success "Development environment synced." - fi -else - step "PHASE 2: Skipped (--no-sync flag provided)" - info "Run 'uv sync' manually when ready to install dependencies." -fi - -# Final Summary -echo -success "Dependency setup complete!" -if [ "$PROD_MODE" = true ]; then - echo " โ€ข Production dependencies locked and synced" -else - echo " โ€ข All dependencies locked and synced" - echo " โ€ข Development tools installed" -fi -echo " โ€ข Lock file: uv.lock" -echo -info "Next steps:" -if [ "$SKIP_SYNC" = false ]; then - echo " โ€ข Run code: uv run python script.py" - echo " โ€ข Run tests: uv run pytest" - echo " โ€ข Type check: uv run mypy ." - echo " โ€ข Check for updates: ./scripts/dependency.sh --check" - echo " โ€ข Update packages: ./scripts/dependency.sh --update" -else - echo " โ€ข Sync environment: uv sync" - echo " โ€ข Then use: uv run python script.py" -fi diff --git a/scripts/setup.sh b/scripts/setup.sh index 1f92b902..d8b52518 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -30,31 +30,6 @@ is_wsl() { grep -qi "microsoft" /proc/sys/kernel/osrelease 2>/dev/null || [[ -n "${WSL_DISTRO_NAME:-}" ]] } -get_ssh_key() { - local host="github.com" - local config_file="$HOME/.ssh/config" - local key="" - - if [ -f "$config_file" ]; then - key=$(awk -v host="$host" ' - tolower($0) ~ tolower("host " host) { in_block=1; next } - in_block && /^host / { in_block=0 } - in_block && tolower($1) == "identityfile" { print $2; exit } - ' "$config_file") - fi - - # If key found, make it absolute path - if [ -n "$key" ]; then - if [[ $key != /* ]]; then - key="$HOME/.ssh/$key" - fi - else - # Fallback to default - key="$HOME/.ssh/id_rsa" - fi - - echo "$key" -} has_cmd() { command -v "$1" >/dev/null 2>&1; } # Universal package installation function @@ -86,25 +61,6 @@ check_system_requirements() { has_cmd python3 || error "Python 3 is not installed. Install Python 3.12+." success "Python $(python3 --version | cut -d' ' -f2) found." - # Ensure the venv module exists for the active python3 - PY_MINOR="$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")" - if python3 -c "import venv" &>/dev/null; then - success "python${PY_MINOR} venv module is available." - else - warn "venv module missing for python${PY_MINOR}." - # Best effort install per distro - if has_cmd apt-get; then - install_package "python${PY_MINOR}-venv" || warn "could not install python${PY_MINOR}-venv automatically" - elif has_cmd dnf; then - # On Fedora venv is in the python base package - install_package "python${PY_MINOR}" || true - elif has_cmd yum; then - install_package "python${PY_MINOR}" || true - else - warn "install the appropriate python venv package manually" - fi - fi - # Check for pip and install if missing if has_cmd pip; then success "pip $(pip --version | cut -d' ' -f2) found." @@ -116,6 +72,21 @@ check_system_requirements() { has_cmd git || error "git is not installed." success "git $(git --version | cut -d' ' -f3) found." + step "verifying uv package manager" + if has_cmd uv; then + success "uv $(uv --version) found." + else + warn "uv not found. installing..." + if has_cmd curl; then + curl -LsSf https://astral.sh/uv/install.sh | sh + # Add uv to PATH for current session + export PATH="$HOME/.local/bin:$PATH" + success "uv installed successfully. Added to PATH." + else + error "curl is required to install uv. Please install curl or uv manually." + fi + fi + if has_cmd direnv; then success "direnv found." else @@ -181,59 +152,16 @@ check_system_requirements() { fi } -setup_virtual_environment() { - section "virtual environment setup" - step "creating Python virtual environment" - if [ -d "$VENV_DIR" ]; then - warn "virtual environment already exists" - if [[ -n "${VENV_CHOICE:-}" ]]; then - info "VENV_CHOICE preset to ${VENV_CHOICE}" - elif [[ "${CI:-}" == "true" || "${NONINTERACTIVE:-}" == "1" ]]; then - VENV_CHOICE=2 - info "non interactive mode active. defaulting to recreate" - else - read -p "Enter 1 to keep, 2 to recreate [default: 2]: " VENV_CHOICE - VENV_CHOICE=${VENV_CHOICE:-2} - fi - if [ "$VENV_CHOICE" = "1" ]; then - success "keeping existing virtual environment" - # shellcheck disable=SC1090 - source "$VENV_DIR/bin/activate" - elif [ "$VENV_CHOICE" = "2" ]; then - warn "removing old virtual environment" - rm -rf "$VENV_DIR" - python3 -m venv "$VENV_DIR" - # shellcheck disable=SC1090 - source "$VENV_DIR/bin/activate" - success "virtual environment created and activated" - else - error "invalid choice" - fi - else - python3 -m venv "$VENV_DIR" - # shellcheck disable=SC1090 - source "$VENV_DIR/bin/activate" - success "virtual environment created and activated" - fi - - step "upgrading pip in virtual environment" - python -m ensurepip --upgrade >/dev/null 2>&1 || true - python -m pip install --upgrade pip - success "pip upgraded to $("$VENV_DIR/bin/python" -m pip --version | awk '{print $2}')." -} - setup_dependencies() { - section "hybrid dependency management" - info "production deps in $PYPROJECT to $PROD_LOCK" - info "development deps in $PYPROJECT extra [dev] to $DEV_LOCK" - info "slim deps in $PYPROJECT without extra [extra] to $SLIM_LOCK" - step "installing pip tools" - python -m pip install pip-tools build wheel - success "pip tools installed" - - step "running dependency setup script" - ./scripts/dependency.sh - success "dependencies installed" + section "dependency management with uv" + info "managing dependencies from $PYPROJECT" + step "locking dependencies with uv" + uv lock + success "uv.lock created" + + step "syncing dependencies with uv" + uv sync + success "dependencies installed from uv.lock" } configure_locale() { @@ -329,14 +257,6 @@ setup_secrets() { success "secrets configuration verified" } -verify_tools() { - section "development tools verification" - step "verifying development tools" - for tool in black ruff mypy pytest beartype; do - if python -m pip show "$tool" >/dev/null 2>&1; then success "$tool installed"; else warn "$tool missing"; fi - done -} - validate_project() { section "project validation" step "running final project validation" @@ -356,9 +276,7 @@ print_completion() { echo -e "\n${GREEN}๐ŸŽ‰ development environment setup complete${RESET}\n" echo -e "${BLUE}installed:${RESET}" echo " โ€ข $VENV_DIR/" - echo " โ€ข core production deps ($PROD_LOCK)" - echo " โ€ข slim production deps ($SLIM_LOCK)" - echo " โ€ข dev tools ($DEV_LOCK)" + echo " โ€ข uv.lock (all dependencies locked and synced)" echo " โ€ข pre commit hooks" echo " โ€ข secret baseline (.secrets.baseline)" echo " โ€ข direnv" @@ -366,17 +284,20 @@ print_completion() { echo " 1. restart terminal or run: source ~/.bashrc" echo " 2. test with: python example/using_secrets.py" echo " 3. start coding" + echo -e "\n${BLUE}common commands:${RESET}" + echo " โ€ข uv sync - sync dependencies from uv.lock" + echo " โ€ข uv lock - update locks (patch + minor updates)" + echo " โ€ข uv lock --upgrade - update locks (with major versions)" + echo " โ€ข uv run pytest - run pytest without activating venv" } setup_development_environment() { check_system_requirements - setup_virtual_environment setup_dependencies configure_locale setup_direnv setup_pre_commit setup_secrets - verify_tools validate_project print_completion } diff --git a/uv.lock b/uv.lock index e8a22fe7..2809f5d1 100644 --- a/uv.lock +++ b/uv.lock @@ -917,27 +917,27 @@ dev = [ [package.metadata] requires-dist = [ - { name = "filelock", specifier = "==3.20.3" }, - { name = "flask", specifier = "==3.1.2" }, - { name = "requests", specifier = "==2.32.5" }, - { name = "urllib3", specifier = "==2.6.3" }, - { name = "werkzeug", specifier = "==3.1.5" }, + { name = "filelock", specifier = ">=3.20.0,<4.0.0" }, + { name = "flask", specifier = ">=3.1.0,<4.0.0" }, + { name = "requests", specifier = ">=2.32.0,<3.0.0" }, + { name = "urllib3", specifier = ">=2.6.0,<3.0.0" }, + { name = "werkzeug", specifier = ">=3.1.0,<4.0.0" }, ] [package.metadata.requires-dev] dev = [ - { name = "beartype", specifier = "==0.22.9" }, - { name = "black", specifier = "==25.12.0" }, - { name = "detect-secrets", specifier = "==1.5.0" }, - { name = "mypy", specifier = "==1.19.1" }, - { name = "nbstripout", specifier = "==0.8.2" }, - { name = "pip-audit", specifier = "==2.10.0" }, - { name = "pre-commit", specifier = "==4.5.1" }, - { name = "pytest", specifier = "==9.0.2" }, - { name = "pytest-beartype", specifier = "==0.2.0" }, - { name = "pytest-cov", specifier = "==7.0.0" }, - { name = "rich", specifier = "==14.2.0" }, - { name = "ruff", specifier = "==0.14.11" }, + { name = "beartype", specifier = ">=0.22.0,<1.0.0" }, + { name = "black", specifier = ">=25.12.0,<26.0.0" }, + { name = "detect-secrets", specifier = ">=1.5.0,<2.0.0" }, + { name = "mypy", specifier = ">=1.19.0,<2.0.0" }, + { name = "nbstripout", specifier = ">=0.8.0,<1.0.0" }, + { name = "pip-audit", specifier = ">=2.10.0,<3.0.0" }, + { name = "pre-commit", specifier = ">=4.5.0,<5.0.0" }, + { name = "pytest", specifier = ">=9.0.0,<10.0.0" }, + { name = "pytest-beartype", specifier = ">=0.2.0,<1.0.0" }, + { name = "pytest-cov", specifier = ">=7.0.0,<8.0.0" }, + { name = "rich", specifier = ">=14.2.0,<15.0.0" }, + { name = "ruff", specifier = ">=0.14.0,<1.0.0" }, ] extra = [] From e872372230d4639a74e59784841ca85844c63ed1 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 14:30:38 +0100 Subject: [PATCH 095/199] Update dependency management to use 'uv sync' and adjust related documentation --- .vscode/settings.json | 5 +++++ CONTRIBUTING.md | 2 +- README.md | 6 ++---- docs/0_setup.md | 8 ++++---- docs/1_usage.md | 4 ++-- docs/5_pre_commit_hooks.md | 5 ++--- scripts/hooks/verify-lockfile.sh | 2 +- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e6a20a5b..43f2ba0d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,11 @@ "--config-file=pyproject.toml" ], "python.linting.mypyEnabled": true, + "python.testing.pytestArgs": [ + "." + ], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, "ruff.args": [ "--config=pyproject.toml" ], diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 50bff42e..abfc4f22 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -139,7 +139,7 @@ For full details, see `docs/0_setup.md`. Key steps include: 1. **System Requirements**: Python 3.12+, pip, curl, git 2. **Virtual Environment**: Remove old `venv`, create and activate new one -3. **Dependency Management**: Use `scripts/dependency.sh` to install prod/dev dependencies and lock files +3. **Dependency Management**: Use `uv sync` to install prod/dev dependencies and lock files 4. **Locale Configuration**: Ensure `en_US.UTF-8` is available 5. **direnv Setup**: (Optional) For automatic environment variable loading 6. **Pre-commit Hooks**: Install and update hooks, run all checks diff --git a/README.md b/README.md index 11505833..535e0f7e 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,9 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co * Always declare packages in `pyproject.toml` only * Use `uv sync` to install dependencies from the lock file * Common commands: - - First time setup: `./scripts/dependency.sh` + - First time setup: `uv sync` - Install deps: `uv sync` - Add a package: Edit `pyproject.toml`, then run `uv lock && uv sync` - - Check for updates: `./scripts/dependency.sh --check` - - Update packages: `./scripts/dependency.sh --update` * The lock file (`uv.lock`) ensures everyone uses the same versions ### Coding @@ -447,7 +445,7 @@ This project is licensed under the MIT License - see the LICENSE file for detail rm -rf venv python3 -m venv venv source venv/bin/activate -./scripts/dependency.sh +uv sync ``` **Pre-commit hook failures:** diff --git a/docs/0_setup.md b/docs/0_setup.md index 38d338e8..db0d7422 100644 --- a/docs/0_setup.md +++ b/docs/0_setup.md @@ -272,24 +272,24 @@ This project uses a hybrid approach to dependency management: You can use the helper script or perform each step manually: -**3.3.1 Usage of `dependency.sh`** +**3.3.1 Using `uv sync`** - Install both production and development dependencies: ``` - ./scripts/dependency.sh + uv sync ``` - Install only production dependencies: ``` - ./scripts/dependency.sh --prod + uv sync --no-dev ``` - Install only development dependencies: ``` - ./scripts/dependency.sh --dev + uv sync --only-dev ``` diff --git a/docs/1_usage.md b/docs/1_usage.md index 7fcbd51d..17c9398b 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -158,8 +158,8 @@ This project uses a hybrid approach where dependencies are declared in `pyprojec * **To Add a Production Dependency**: 1. Add the package name (e.g., `"pandas"`) to the `dependencies` list in `pyproject.toml`. - 2. Run the dependency script to update the lockfile: `./scripts/dependency.sh` + 2. Run `uv lock && uv sync` to update the lockfile * **To Add a Development-Only Dependency**: 1. Add the package name (e.g., `"ipykernel"`) to the `dev` list under `[project.optional-dependencies]` in `pyproject.toml`. - 2. Run the dependency script: `./scripts/dependency.sh` + 2. Run `uv lock && uv sync` diff --git a/docs/5_pre_commit_hooks.md b/docs/5_pre_commit_hooks.md index 7195a11d..76624452 100644 --- a/docs/5_pre_commit_hooks.md +++ b/docs/5_pre_commit_hooks.md @@ -107,13 +107,12 @@ Make sure `.secrets.baseline` stays committed and up to date. ### 3.3.4 Enforcing Dependency Workflow -The custom `prevent-manual-requirements-edits` hook ensures that `requirements.txt` is only modified via `pip-compile` and never edited manually. +The custom `prevent-manual-requirements-edits` hook ensures that the lock file is only modified via `uv lock` and never edited manually. To add a package: ```bash -echo "package-name" >> requirements.in -./scripts/dependency.sh +uv add package-name ``` --- diff --git a/scripts/hooks/verify-lockfile.sh b/scripts/hooks/verify-lockfile.sh index 0b58f235..9f9b29dc 100755 --- a/scripts/hooks/verify-lockfile.sh +++ b/scripts/hooks/verify-lockfile.sh @@ -37,7 +37,7 @@ normalize_requirements() { if ! diff -q <(normalize_requirements "$LOCKFILE_TO_CHECK") <(normalize_requirements "$TEMP_LOCKFILE") > /dev/null; then echo "โŒ Error: '$LOCKFILE_TO_CHECK' has been manually edited or is out of date." >&2 echo " It does not match the expected output based on '$PYPROJECT_FILE'." >&2 - echo " To fix this, run 'rm $LOCKFILE_TO_CHECK' and then './scripts/dependency.sh' and commit the resulting changes." >&2 + echo " To fix this, run 'uv lock' and commit the resulting changes." >&2 rm "$TEMP_LOCKFILE" # Always clean up exit 1 fi From 69a675566926d125e25af8548aa3af3b8133b4d3 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 14:40:20 +0100 Subject: [PATCH 096/199] Update project metadata and improve interpreter path configuration --- .vscode/settings.json | 1 + pyproject.toml | 17 +++++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 43f2ba0d..ee627984 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "python.analysis.typeCheckingMode": "standard", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "python.linting.enabled": true, "python.linting.mypyArgs": [ "--config-file=pyproject.toml" diff --git a/pyproject.toml b/pyproject.toml index e9963b1b..f285b199 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,11 @@ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" +[project.urls] +Homepage = "https://github.com/TECHLETES/python_basic_template" +Repository = "https://github.com/TECHLETES/python_basic_template" +Issues = "https://github.com/TECHLETES/python_basic_template/issues" + [project] name = "python-basic-template" version = "0.1.0" @@ -9,7 +14,6 @@ description = "A basic Python project template with full DevOps tooling support. readme = "README.md" requires-python = ">=3.12,<4.0" keywords = ["devops", "template", "python"] -license = {text = "MIT"} authors = [ {name = "Thom Ezechiels", email = "thom@techletes.ai"}, ] @@ -64,12 +68,6 @@ dev = [ "rich>=14.2.0,<15.0.0", ] - -[project.urls] -Homepage = "https://github.com/TECHLETES/python_basic_template" -Repository = "https://github.com/TECHLETES/python_basic_template" -Issues = "https://github.com/TECHLETES/python_basic_template/issues" - [tool.setuptools.packages.find] where = ["."] include = ["utils*", "example*"] @@ -79,8 +77,8 @@ exclude = ["tests*"] line-length = 88 target-version = ["py312"] -[tool.pytest.ini_options] -minversion = "6.0" +[tool.pytest] +minversion = "9.0" addopts = [ "--strict-markers", "--strict-config", @@ -139,7 +137,6 @@ warn_return_any = true warn_unreachable = true no_implicit_optional = true allow_redefinition = true -show_error_codes = true show_column_numbers = true pretty = true warn_unused_configs = true From 729ab58ff30f658e89e3a4efce0ad0d1d7d0df6a Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 14:42:35 +0100 Subject: [PATCH 097/199] Refactor setup script to use virtual environment for pre-commit and detect-secrets commands --- .secrets.baseline | 2 +- scripts/setup.sh | 32 +++++--------------------------- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index da31ed89..a35695bb 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -146,5 +146,5 @@ } ] }, - "generated_at": "2026-03-03T13:21:38Z" + "generated_at": "2026-03-03T13:41:59Z" } diff --git a/scripts/setup.sh b/scripts/setup.sh index d8b52518..b2af7d26 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -208,15 +208,14 @@ setup_pre_commit() { section "pre commit hooks setup" step "installing pre commit hooks" if [[ -f ".pre-commit-config.yaml" ]]; then - python -m pip install pre-commit detect-secrets || true - pre-commit install + $VENV_DIR/bin/pre-commit install success "pre commit hooks installed" step "updating pre commit repos" - pre-commit autoupdate + $VENV_DIR/bin/pre-commit autoupdate success "pre commit hooks updated" step "running pre commit on all files" - pre-commit run --all-files || warn "some pre commit checks failed" + $VENV_DIR/bin/pre-commit run --all-files || warn "some pre commit checks failed" else warn ".pre-commit-config.yaml not found. skipping" fi @@ -224,36 +223,15 @@ setup_pre_commit() { setup_secrets() { section "secret management setup" - step "checking 1password cli" - if has_cmd op; then - success "1password cli is installed" - if op account list >/dev/null 2>&1; then - success "1password cli is connected" - else - warn "1password cli not signed in" - fi - else - # In WSL users sometimes rely on Windows 1Password - if is_wsl; then - warn "op not found in WSL. if you use Windows 1Password, install op in WSL or skip this" - else - error "1Password CLI not found. install and configure before secrets workflow" - fi - fi - - # Ensure detect-secrets is available before using it - if ! command -v detect-secrets >/dev/null 2>&1; then - python -m pip install detect-secrets - fi step "initializing secrets baseline" if [ -f ".secrets.baseline" ]; then warn "existing .secrets.baseline found. backing up" cp .secrets.baseline .secrets.baseline.bak fi - detect-secrets scan > .secrets.baseline + $VENV_DIR/bin/detect-secrets scan > .secrets.baseline success "secrets baseline created" - detect-secrets audit .secrets.baseline || warn "manual review of baseline recommended" + $VENV_DIR/bin/detect-secrets audit .secrets.baseline || warn "manual review of baseline recommended" success "secrets configuration verified" } From 304c75dd8d9e26cbee38391ffa5cf009c1d5010b Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 14:56:23 +0100 Subject: [PATCH 098/199] Enhance pre-commit configuration with additional hooks for import sorting, syntax modernization, spell checking, security scanning, and docstring validation; update dependencies in pyproject.toml and uv.lock --- .pre-commit-config.yaml | 47 ++++++++++++++++++++ .secrets.baseline | 2 +- example/using_secrets.py | 2 +- pyproject.toml | 5 +++ utils/__init__.py | 2 + utils/secrets.py | 8 +++- uv.lock | 94 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 156 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 059fb8a3..cc6a89cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,6 +25,7 @@ repos: - id: check-symlinks - id: check-yaml - id: check-added-large-files + - id: check-merge-conflict - id: detect-private-key - id: pretty-format-json args: ["--autofix"] @@ -62,6 +63,24 @@ repos: name: Type check with mypy args: ["--config-file=pyproject.toml"] + # Import sorting with isort (must run before formatters) + - repo: https://github.com/PyCQA/isort + rev: 8.0.1 + hooks: + - id: isort + name: isort (import sorting) + args: [--profile=black] + types: [python] + + # Modernize Python syntax + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + name: pyupgrade (modernize Python syntax) + args: [--py312-plus] + types: [python] + # Linting and formatting with ruff - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.4 @@ -71,6 +90,34 @@ repos: args: [--fix] types: [python] + # Spell checking + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + name: codespell (spell check) + types: [python] + args: [--skip=*.lock] + + # Security scanning + - repo: https://github.com/PyCQA/bandit + rev: 1.9.4 + hooks: + - id: bandit + name: bandit (security check) + args: [-ll, --skip, B101] + exclude: ^tests/ + types: [python] + + # Docstring validation + - repo: https://github.com/PyCQA/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + name: pydocstyle (docstring validation) + exclude: ^(tests/|example/) + types: [python] + # Testing with pytest (locally managed by uv) - repo: local hooks: diff --git a/.secrets.baseline b/.secrets.baseline index a35695bb..b091e406 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -146,5 +146,5 @@ } ] }, - "generated_at": "2026-03-03T13:41:59Z" + "generated_at": "2026-03-03T13:51:03Z" } diff --git a/example/using_secrets.py b/example/using_secrets.py index 5d413e13..9bd30b35 100644 --- a/example/using_secrets.py +++ b/example/using_secrets.py @@ -3,7 +3,7 @@ def main() -> None: # Use the get_secret function to retrieve your secret. - # To find the path to your secret go to 1Password Interace > your item > click dropdown next to field > "Copy secret reference" + # To find the path to your secret go to 1Password Interface > your item > click dropdown next to field > "Copy secret reference" api_key = get_secret("op://Shared with all/RAGFLow API Key/credential") if api_key: print(f"API_KEY is set: {api_key}") diff --git a/pyproject.toml b/pyproject.toml index f285b199..72b10a3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,11 @@ dev = [ "pytest-beartype>=0.2.0,<1.0.0", "pip-audit>=2.10.0,<3.0.0", "rich>=14.2.0,<15.0.0", + "isort>=5.13.0,<6.0.0", + "pyupgrade>=3.15.0,<4.0.0", + "codespell>=2.2.0,<3.0.0", + "bandit>=1.7.0,<2.0.0", + "pydocstyle>=6.3.0,<7.0.0", ] [tool.setuptools.packages.find] diff --git a/utils/__init__.py b/utils/__init__.py index 1f1a0509..8673f0d0 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,3 +1,5 @@ +"""Utility package with runtime type checking and secret management.""" + import os from beartype.claw import beartype_this_package diff --git a/utils/secrets.py b/utils/secrets.py index ed738db4..23fe949c 100644 --- a/utils/secrets.py +++ b/utils/secrets.py @@ -1,5 +1,9 @@ -# A tiny helper module to load secrets at runtime if you canโ€™t rely on direnv -# This uses the 1Password CLI to fetch secrets on-the-fly in any script or app +"""Helper module to load secrets at runtime using 1Password CLI. + +This module provides utilities to fetch secrets on-the-fly in scripts and apps +when you can't rely on direnv. It uses the 1Password CLI to securely retrieve +and manage secrets at runtime. +""" import os import re diff --git a/uv.lock b/uv.lock index 2809f5d1..333e1fe3 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + [[package]] name = "beartype" version = "0.22.9" @@ -175,6 +190,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] +[[package]] +name = "codespell" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -367,6 +391,15 @@ 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 = "isort" +version = "5.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -826,6 +859,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, ] +[[package]] +name = "pydocstyle" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "snowballstemmer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/d5385ca59fd065e3c6a5fe19f9bc9d5ea7f2509fa8c9c22fb6b2031dd953/pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1", size = 36796, upload-time = "2023-01-17T20:29:19.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/ea/99ddefac41971acad68f14114f38261c1f27dac0b3ec529824ebc739bdaa/pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", size = 38038, upload-time = "2023-01-17T20:29:18.094Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -901,16 +946,21 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "bandit" }, { name = "beartype" }, { name = "black" }, + { name = "codespell" }, { name = "detect-secrets" }, + { name = "isort" }, { name = "mypy" }, { name = "nbstripout" }, { name = "pip-audit" }, { name = "pre-commit" }, + { name = "pydocstyle" }, { name = "pytest" }, { name = "pytest-beartype" }, { name = "pytest-cov" }, + { name = "pyupgrade" }, { name = "rich" }, { name = "ruff" }, ] @@ -926,16 +976,21 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "bandit", specifier = ">=1.7.0,<2.0.0" }, { name = "beartype", specifier = ">=0.22.0,<1.0.0" }, { name = "black", specifier = ">=25.12.0,<26.0.0" }, + { name = "codespell", specifier = ">=2.2.0,<3.0.0" }, { name = "detect-secrets", specifier = ">=1.5.0,<2.0.0" }, + { name = "isort", specifier = ">=5.13.0,<6.0.0" }, { name = "mypy", specifier = ">=1.19.0,<2.0.0" }, { name = "nbstripout", specifier = ">=0.8.0,<1.0.0" }, { name = "pip-audit", specifier = ">=2.10.0,<3.0.0" }, { name = "pre-commit", specifier = ">=4.5.0,<5.0.0" }, + { name = "pydocstyle", specifier = ">=6.3.0,<7.0.0" }, { name = "pytest", specifier = ">=9.0.0,<10.0.0" }, { name = "pytest-beartype", specifier = ">=0.2.0,<1.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0,<8.0.0" }, + { name = "pyupgrade", specifier = ">=3.15.0,<4.0.0" }, { name = "rich", specifier = ">=14.2.0,<15.0.0" }, { name = "ruff", specifier = ">=0.14.0,<1.0.0" }, ] @@ -950,6 +1005,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, ] +[[package]] +name = "pyupgrade" +version = "3.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tokenize-rt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/a1/dc63caaeed232b1c58eae1b7a75f262d64ab8435882f696ffa9b58c0c415/pyupgrade-3.21.2.tar.gz", hash = "sha256:1a361bea39deda78d1460f65d9dd548d3a36ff8171d2482298539b9dc11c9c06", size = 45455, upload-time = "2025-11-19T00:39:48.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8c/433dac11910989a90c40b10149d07ef7224232236971a562d3976790ec53/pyupgrade-3.21.2-py2.py3-none-any.whl", hash = "sha256:2ac7b95cbd176475041e4dfe8ef81298bd4654a244f957167bd68af37d52be9f", size = 62814, upload-time = "2025-11-19T00:39:46.958Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1145,6 +1212,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, ] +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + [[package]] name = "sortedcontainers" version = "2.4.0" @@ -1154,6 +1230,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "stevedore" +version = "5.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, +] + +[[package]] +name = "tokenize-rt" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/ed/8f07e893132d5051d86a553e749d5c89b2a4776eb3a579b72ed61f8559ca/tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6", size = 5476, upload-time = "2025-05-23T23:48:00.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44", size = 6004, upload-time = "2025-05-23T23:47:58.812Z" }, +] + [[package]] name = "tomli" version = "2.4.0" From 3a2afc4d51e45c0a0a96ffa16d1664d31e642d07 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 14:58:43 +0100 Subject: [PATCH 099/199] Add comprehensive troubleshooting guide for pre-commit hooks --- docs/6_pre_commit_troubleshooting.md | 774 +++++++++++++++++++++++++++ 1 file changed, 774 insertions(+) create mode 100644 docs/6_pre_commit_troubleshooting.md diff --git a/docs/6_pre_commit_troubleshooting.md b/docs/6_pre_commit_troubleshooting.md new file mode 100644 index 00000000..0622f557 --- /dev/null +++ b/docs/6_pre_commit_troubleshooting.md @@ -0,0 +1,774 @@ +# Pre-Commit Hooks Troubleshooting Guide + +This guide covers common issues and solutions for all pre-commit hooks used in this Python template. + +## General Pre-Commit Issues + +### Pre-Commit Won't Run at All + +**Problem:** `pre-commit install` fails or hooks don't execute on commit. + +**Solutions:** +```bash +# Reinstall hooks +pre-commit install --install-hooks + +# Verify configuration is valid +pre-commit validate-config + +# Check if .pre-commit-config.yaml exists +ls -la .pre-commit-config.yaml + +# Run manually to test +pre-commit run --all-files +``` + +### "Command Not Found" or "hook id not found" + +**Problem:** A specific hook can't be located. + +**Solutions:** +```bash +# Clear pre-commit cache and reinstall +pre-commit clean +pre-commit gc +pre-commit install --install-hooks + +# Verify the hook ID is correct in config +pre-commit run --all-files # Should not error + +# Update to latest versions +pre-commit autoupdate +``` + +### Hooks Failing on First Commit After Install + +**Problem:** All files fail even though code should be valid. + +**Solutions:** +1. Pre-commit flags **staging state** issues. Ensure all changes are staged: + ```bash + git add . + ``` + +2. Run hooks manually to verify they work: + ```bash + pre-commit run --all-files + ``` + +3. Some hooks modify files. Review changes: + ```bash + git diff + git add . + git commit -m "Fix pre-commit violations" + ``` + +### Hook Modifies Files But Commit Still Fails + +**Problem:** Hook auto-fixes code, but you still can't commit. + +**Solutions:** +- This is normal! The hook found and fixed issues. Simply: + ```bash + git add + git commit + ``` + +--- + +## Import & Code Formatting Hooks + +### isort (Import Sorting) + +**Problem:** isort conflicts with Black formatting + +**Solution:** Ensure Black profile is enabled: +```yaml +- repo: https://github.com/PyCQA/isort + hooks: + - id: isort + args: [--profile=black] # Must specify Black profile! +``` + +**Problem:** isort changes files every run (unstable) + +**Solutions:** +```bash +# Verify pyproject.toml isort config (if any) +cat pyproject.toml | grep -A 10 "\[tool.isort\]" + +# Clear cache +rm -rf .isort.cache + +# Run with verbose output +pre-commit run isort --all-files --verbose + +# Common fix: ensure consistent profile +run: uv run isort --profile=black . +``` + +**Problem:** "Known third party modules not recognized" + +**Solution:** Add to `.isort.cfg` or `pyproject.toml`: +```toml +[tool.isort] +profile = "black" +known_first_party = ["utils", "example"] # Your local packages +``` + +### Black (Code Formatter) + +**Problem:** Black conflicts with other formatters (ruff, isort) + +**Solution:** Ensure execution order and compatible configs: +1. isort runs FIRST +2. Then Black +3. Then ruff + +**Problem:** "Cannot parse file" or "Unexpected token" + +**Solutions:** +```bash +# Check Python syntax +python -m py_compile path/to/file.py + +# Verify file encoding +file -i path/to/file.py # Should show utf-8 + +# Run Black directly for better error messages +uv run black path/to/file.py --check --diff +``` + +**Problem:** Black changes line breaks inconsistently + +**Solution:** Ensure `pyproject.toml` has Black config: +```toml +[tool.black] +line-length = 88 +target-version = ["py312"] +``` + +### Ruff (Linting & Formatting) + +**Problem:** Ruff conflicts with isort or Black + +**Solution:** Disable overlapping rules in `pyproject.toml`: +```toml +[tool.ruff] +line-length = 88 + +[tool.ruff.lint] +# Ensure ruff doesn't override isort +ignore = ["I"] # Ignore import sorting (handled by isort) + +[tool.ruff.lint.isort] +profile = "black" +``` + +**Problem:** Ruff fails with "E501: line too long" even after Black + +**Solution:** Check if ruff's line length differs from Black: +```toml +[tool.ruff] +line-length = 88 # Match Black's line length + +[tool.ruff.lint] +extend-ignore = ["E203"] # Compatibility with Black +``` + +**Problem:** Files keep changing between ruff and Black + +**Solutions:** +```bash +# Run with verbose output +uv run ruff check --fix path/to/file.py --show-fixes + +# Verify ruff config +ruff config + +# Try running in isolation +ruff check --fix . +black . +isort . +``` + +### pyupgrade (Python Modernization) + +**Problem:** Changes not applied or reverting + +**Solution:** Ensure Python version target matches: +```yaml +- repo: https://github.com/asottile/pyupgrade + hooks: + - id: pyupgrade + args: [--py312-plus] # Must match your project's Python version! +``` + +**Problem:** Breaks type hints or compatibility + +**Solutions:** +```bash +# Run with verbose to see what changed +uv run pyupgrade --py312-plus --diff path/to/file.py + +# Check for specific issues +grep -r "Union\[" . # pyupgrade changes Union to | +grep -r "Optional" . # pyupgrade changes Optional to x | None +``` + +--- + +## Type Checking + +### mypy (Type Checker) + +**Problem:** "error: Skipping analyzing... file not found" or missing stubs + +**Solutions:** +```bash +# Install missing type stubs +uv sync # Re-sync dependencies (includes type packages) + +# Create py.typed file for your package +touch utils/py.typed +touch example/py.typed + +# Check mypy config +cat pyproject.toml | grep -A 20 "\[tool.mypy\]" +``` + +**Problem:** "error: Cannot find implementation or library stub" for third-party package + +**Solutions:** +```bash +# Try installing types package +pip install types- + +# Or suppress warnings for that package +# At top of file: +# type: ignore[import] +``` + +**Problem:** "error: Function is missing return type annotation" + +**Solution:** Add type hints to your functions: +```python +def my_function(x: int) -> str: + """Function with type hints.""" + return str(x) +``` + +Or configure mypy to be less strict: +```toml +[tool.mypy] +disallow_untyped_defs = false +``` + +**Problem:** Mypy passes locally but fails in CI + +**Solutions:** +```bash +# Clear cache and reinstall +rm -rf .mypy_cache +uv sync + +# Verify mypy sees all files +mypy --config-file=pyproject.toml --show-traceback . + +# Check Python version match +python --version +# Ensure version >= 3.12 +``` + +--- + +## Spell Checking + +### codespell (Spell Checker) + +**Problem:** False positives (real words marked as typos) + +**Solutions:** +```bash +# Add exceptions to pyproject.toml +[tool.codespell] +ignore-words-list = "crate,nd,sav,ser" +skip = "*.lock,*.log" + +# Or add inline exception +# cSpell:ignore myword +``` + +**Problem:** Checks binary files or shouldn't-check files + +**Solution:** Exclude in hook configuration: +```yaml +- id: codespell + args: [--skip=*.lock,*.pyc,htmlcov/*] + exclude: ^(htmlcov/|\.git) +``` + +**Problem:** Hook runs on all files, not just changed ones + +**Solution:** Add type filter: +```yaml +- id: codespell + types: [python, markdown] # Only check these types +``` + +--- + +## Security Scanning + +### Bandit (Security Checker) + +**Problem:** "Issue found" but the code is safe + +**Solutions:** +```bash +# Suppress specific issue in code +def eval_bad(): + eval("1 + 1") # noqa: S307 (safely ignored) + +# Or in pyproject.toml +[tool.bandit] +exclude_dirs = ["tests"] +skips = ["B101"] # Skip assertion tests +``` + +**Problem:** Bandit checks test files when it shouldn't + +**Solution:** Ensure config excludes tests: +```yaml +- id: bandit + exclude: ^tests/ + args: [-ll, --skip, B101] # Only high/medium severity +``` + +**Problem:** False positive on string with "exec" or "eval" + +**Solution:** Use inline comment: +```python +# This is just documentation, not actual execution +example = "exec(code)" # noqa: S303 +``` + +### detect-secrets (Secret Detection) + +**Problem:** Real secrets incorrectly flagged or false positives + +**Solutions:** +```bash +# Audit and update baseline +detect-secrets scan > .secrets.baseline +detect-secrets audit .secrets.baseline + +# If certain patterns are always false positives +detect-secrets scan --all-files > .secrets.baseline +``` + +**Problem:** Baseline file corrupted or out of sync + +**Fixes:** +```bash +# Regenerate baseline +rm .secrets.baseline +detect-secrets scan > .secrets.baseline + +# Verify it's valid JSON +python -m json.tool .secrets.baseline > /dev/null +``` + +**Problem:** Valid test credentials flagged as secrets + +**Solution:** Add exemption in `.secrets.baseline`: +```bash +# Or in hook, exclude test files +- id: detect-secrets + args: ["--baseline", ".secrets.baseline"] + exclude: ^tests/|\.txt$ +``` + +### pip-audit (Dependency Vulnerabilities) + +**Problem:** Known vulnerability that's not fixable + +**Solutions:** +```bash +# Ignore specific CVE (temporary) +- id: pip-audit + args: ["--ignore", "CVE-2026-1234"] + +# Or update dependencies +uv lock --upgrade +uv sync +``` + +**Problem:** "pyright", "autopep8" or old packages show vulnerabilities + +**Solution:** Review dependencies in `pyproject.toml`: +```bash +# Check which package needs updating +uv pip show + +# Update if available +uv lock --upgrade-package +``` + +--- + +## Docstring Validation + +### pydocstyle (Docstring Checker) + +**Problem:** "Missing docstring in public module/package" + +**Solutions:** +Add module-level docstring at top of file: +```python +"""Package/module description. + +Optional longer description with more details. +""" +import os +``` + +**Problem:** "First line should end with a period" + +**Solution:** Ensure docstring first line ends with period and is a summary: +```python +def my_function(): + """Short summary that ends with period.""" + pass +``` + +**Problem:** "No summary line" or "blank line after summary" + +**Solution:** Follow proper docstring format: +```python +def my_function(x: int) -> str: + """Summary line in present tense. + + Args: + x: Description of parameter. + + Returns: + Description of return value. + """ + return str(x) +``` + +**Problem:** Docstring errors in test code + +**Solution:** Exclude tests in hook config: +```yaml +- id: pydocstyle + exclude: ^tests/ + types: [python] +``` + +--- + +## Notebook Hooks + +### nbstripout (Notebook Output Stripping) + +**Problem:** Notebook outputs not being stripped + +**Solutions:** +```bash +# Run directly to debug +uv run nbstripout path/to/notebook.ipynb --diff + +# Verify hook is in config +grep -A 5 "nbstripout" .pre-commit-config.yaml + +# Clear Jupyter cache +rm -rf ~/.jupyter/nbconvert_cache +``` + +**Problem:** Hook fails on special notebooks + +**Solution:** Add to exclude pattern: +```yaml +- id: nbstripout-autoadd + exclude: ^example/special_notebook\.ipynb$ +``` + +### jupytext (Notebook/Script Sync) + +**Problem:** `.py` and `.ipynb` versions diverge + +**Solutions:** +```bash +# Force sync one direction +uv run jupytext --sync path/to/notebook.ipynb + +# Check sync status +uv run jupytext --set-formats ipynb,py path/to/notebook.ipynb + +# Verify config in pyproject.toml +cat pyproject.toml | grep -A 5 "\[tool.jupytext\]" +``` + +**Problem:** Merge conflicts between .py and .ipynb versions + +**Solution:** Handle in pre-commit by setting explicit format: +```yaml +- id: jupytext + args: [--sync, --fmt, "ipynb,py:percent"] + require_serial: true +``` + +--- + +## Test Execution + +### pytest (Test Runner) + +**Problem:** Tests pass locally but fail in pre-commit + +**Solutions:** +```bash +# Run exactly as pre-commit does +uv run pytest --cov --cov-fail-under=80 --beartype-packages=utils,example + +# Check coverage report +open htmlcov/index.html + +# Verify all tests discovered +uv run pytest --collect-only +``` + +**Problem:** Coverage too low (below 80%) + +**Solutions:** +```bash +# See uncovered lines +open htmlcov/index.html + +# Lower coverage requirement temporarily +# (in .pre-commit-config.yaml during development) +args: [--cov, --cov-fail-under=60] + +# Or add coverage exemptions: +# In code: +def never_called(): # pragma: no cover + pass +``` + +**Problem:** Tests timeout or hang + +**Solution:** Add timeout to hook: +```yaml +- id: pytest + timeout: 60 # seconds +``` + +--- + +## Basic File Checkers + +### check-json, check-yaml, check-toml + +**Problem:** "Invalid JSON/YAML/TOML" + +**Solutions:** +```bash +# Validate specific files +python -m json.tool file.json # JSON +python -c "import yaml; yaml.safe_load(open('file.yaml'))" # YAML +python -c "import tomli; tomli.loads(open('file.toml').read())" # TOML + +# Common issues: +# - Trailing commas in JSON +# - Tabs instead of spaces in YAML +# - Unquoted strings in TOML +``` + +### check-added-large-files + +**Problem:** False positive or need to commit large file + +**Solutions:** +```bash +# Add exception +- id: check-added-large-files + args: ["--maxkb=600"] # Allow up to 600KB + +# Or use git-lfs for large files +git lfs track "*.bin" +``` + +### trailing-whitespace, end-of-file-fixer + +**Problem:** Hooks keep modifying files + +**Solution:** These hooks automatically fix issues. Just stage the changes: +```bash +git add . +git commit # Should now pass +``` + +### detect-private-key + +**Problem:** False positive on legitimate keys + +**Solution:** Add exclusion: +```yaml +- id: detect-private-key + exclude: ^path/to/test/keys/ + args: ["--allow-missing-credentials"] +``` + +--- + +## Performance Issues + +### Hooks Are Too Slow + +**Solutions:** +```bash +# Run only changed files +pre-commit run --hook-stage commit + +# Parallelization +# Add to .pre-commit-config.yaml top level: +# default_language_version: +# python: python3.12 + +# Use --files to limit scope +pre-commit run --files="src/**/*.py" + +# Disable slower hooks locally +SKIP=mypy,pytest git commit +``` + +### One Hook Takes Most Time + +**Solution:** +```bash +# Profile individual hook +time pre-commit run --all-files + +# Consider moving to pre-push stage +- id: pytest + stages: [pre-push] # Slower tests run on push, not commit +``` + +--- + +## Skipping Hooks + +### Skip Specific Hook in Commit + +```bash +SKIP=mypy,pytest git commit +``` + +### Temporarily Disable Hook + +Edit `.pre-commit-config.yaml` and comment out the hook, then: +```bash +pre-commit install +``` + +### Skip Pre-Commit Entirely + +```bash +git commit --no-verify # โš ๏ธ Use sparingly! +``` + +--- + +## Debugging & Getting Help + +### Get Full Verbose Output + +```bash +# Run with max verbosity +pre-commit run --all-files --verbose + +# Show what each hook does +pre-commit run --all-files --verbose +``` + +### Dry Run Without Modifying Files + +```bash +# Check what would change +pre-commit run --all-files --dry-run 2>/dev/null || true +``` + +### Clean Up & Reset + +```bash +# Clear all cache +pre-commit clean +pre-commit gc + +# Reinstall from scratch +rm -rf .pre-commit +pre-commit install --install-hooks --overwrite +``` + +### Check Hook Versions + +```bash +# See installed hook versions +pre-commit --version + +# List all hook repos and versions +grep "rev:" .pre-commit-config.yaml +``` + +--- + +## Common Multi-Hook Issues + +### Import/Format/Lint Cycle (A Modifies, B Undoes, C Redoes) + +**Problem:** isort, Black, and ruff keep changing files back and forth. + +**Solution:** Ensure proper hook order: +```yaml +# 1. Import sorting FIRST +- repo: https://github.com/PyCQA/isort + hooks: + - id: isort + args: [--profile=black] + +# 2. Python modernization +- repo: https://github.com/asottile/pyupgrade + hooks: + - id: pyupgrade + args: [--py312-plus] + +# 3. Linting + formatting LAST +- repo: https://github.com/astral-sh/ruff-pre-commit + hooks: + - id: ruff + args: [--fix] +``` + +And ensure config compatibility: +```toml +[tool.isort] +profile = "black" + +[tool.black] +line-length = 88 + +[tool.ruff] +line-length = 88 +ignore = ["I"] # Don't let ruff handle imports +``` + +--- + +## Next Steps + +- Review hook documentation: `pre-commit try-repo ` +- Update all hooks: `pre-commit autoupdate` +- Profile performance: Run individual hooks with `time` command +- Check project issues/FAQs in each hook's GitHub repository From 350da7f67c4ae0c46e869f0103e9882d68f39d00 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 15:15:03 +0100 Subject: [PATCH 100/199] Update CI configuration and add Copilot instructions for project setup and guidelines --- .github/copilot-instructions.md | 450 ++++++++++++++++++++++++++++++++ .github/dependabot.yml | 47 ++++ .github/workflows/ci.yml | 53 ++-- 3 files changed, 525 insertions(+), 25 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/dependabot.yml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..fa35a50e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,450 @@ +# Copilot Instructions for Python Basic Template (TECHLETES) + +## Project Overview + +A production-ready Python project template for TECHLETES employees. Features modern dependency management (`uv`), secure secret management (1Password CLI integration), comprehensive development tooling, and mandatory pre-commit hooks for code quality. + +**Key Characteristics:** +- Python 3.12+ required +- Reproducible builds via `uv` lock file +- Runtime + static type checking (beartype + mypy) +- 16+ pre-commit hooks enforcing code quality +- Full CI/CD pipeline with GitHub Actions +- Internal use only (TECHLETES employees with 1Password access) + +--- + +## Essential Rules + +### Rule 1: Secrets Management +**Never put secrets in code, environment files, or notebooks.** +- Use `from utils.secrets import get_secret()` to load secrets from 1Password +- Pattern: `get_secret("op://Vault Name/Item Name/field_name")` +- Example: `api_key = get_secret("op://Engineering/GitHub/token")` +- 1Password CLI (`op` command) must be installed and authenticated +- See `docs/2_secret_management.md` for setup details + +### Rule 2: Dependency Management +**Never run `pip install` directly.** +- Always declare packages in `pyproject.toml` under `[dependencies]` or `[dependency-groups]` +- Workflow: Edit `pyproject.toml` โ†’ `uv lock` โ†’ `uv sync` +- Why: Ensures all developers use identical versions (reproducibility) +- Versions use range syntax: `package>=X.Y.Z, dict[str, int]: + return {item: len(item) for item in items} + ``` + +### Rule 4: Commit and Virtual Environment +- **Always activate the virtual environment before committing** + - Linux/macOS: `source .venv/bin/activate` + - Windows: `.venv\Scripts\Activate.ps1` + - Or use direnv: `direnv allow` (auto-activates on `cd`) +- Pre-commit hooks require packages from the venv to run +- If hooks fail, fix issues and recommitโ€”this is normal and protects the repo +- Skip with `git commit --no-verify` only in emergencies (not recommended) + +--- + +## Quick Start + +### Initial Setup (First Time) +```bash +git clone https://github.com/TECHLETES/python_basic_template.git +cd python_basic_template +./scripts/setup.sh # Linux/macOS +.\scripts\setup.ps1 # Windows +``` + +### Activate Virtual Environment (Every Session) +```bash +source .venv/bin/activate # Linux/macOS +.venv\Scripts\Activate.ps1 # Windows +direnv allow # If using direnv +``` + +### Daily Commands + +| Task | Command | +|------|---------| +| **Install/update deps** | `uv sync` | +| **Add new package** | Edit `pyproject.toml`, then `uv lock && uv sync` | +| **Run tests** | `pytest` or `make test` | +| **Run tests with coverage** | `pytest --cov` | +| **Format code** | `uv run black .` | +| **Lint and fix** | `uv run ruff check . --fix` | +| **Type check** | `uv run mypy . --config-file=pyproject.toml` | +| **Pre-commit all files** | `pre-commit run --all-files` | +| **Load secret** | `from utils.secrets import get_secret; api_key = get_secret("op://...")` | + +--- + +## Project Structure & Key Files + +``` +utils/ # Production code (shared utilities) +โ”œโ”€โ”€ __init__.py # Enables runtime type checking +โ”œโ”€โ”€ secrets.py # 1Password CLI wrapper (get_secret) +โ””โ”€โ”€ [other modules] + +example/ # Example code & notebooks +โ”œโ”€โ”€ using_secrets.py # Demo of secret loading +โ””โ”€โ”€ using_secrets.ipynb # Jupyter with auto-stripped outputs + +tests/ +โ””โ”€โ”€ utils/ # Test structure mirrors production + โ””โ”€โ”€ test_*.py # Unit tests (prefix: test_) + +scripts/ +โ”œโ”€โ”€ setup.sh / setup.ps1 # Environment initialization +โ”œโ”€โ”€ new-branch.sh # Git workflow helper +โ””โ”€โ”€ hooks/ # Pre-commit hook scripts + โ”œโ”€โ”€ black-autoformat.sh + โ”œโ”€โ”€ nbstripout-autoadd.sh # Auto-strip notebook outputs + โ”œโ”€โ”€ precommit_pytest.sh # Run tests on commit + โ””โ”€โ”€ verify-lockfile.sh # Ensure uv.lock is valid + +.github/ +โ”œโ”€โ”€ workflows/ci.yml # GitHub Actions: lint, type check, test, coverage +โ”œโ”€โ”€ pull_request_template.md # PR checklist (includes security review) +โ””โ”€โ”€ dependabot.yml # Automated dependency updates + +docs/ +โ”œโ”€โ”€ 0_setup.md # Setup walkthrough +โ”œโ”€โ”€ 1_usage.md # Daily development guide +โ”œโ”€โ”€ 2_secret_management.md # 1Password integration +โ”œโ”€โ”€ 3_dependency_management.md # Using uv +โ”œโ”€โ”€ 4_code_quality.md # Typing & linting tools +โ”œโ”€โ”€ 5_pre_commit_hooks.md # Hook explanations +โ””โ”€โ”€ 6_pre_commit_troubleshooting.md + +pyproject.toml # Single source of truth (metadata, deps, tool configs) +uv.lock # Locked dependency versions (auto-generated, commit it) +.pre-commit-config.yaml # 16+ pre-commit hooks configuration +``` + +--- + +## Key Conventions + +### Code Organization +- **Production code:** Place in `utils/` or `example/` packages +- **Tests:** Mirror production structureโ€”test `utils/foo.py` in `tests/utils/test_foo.py` +- **Test naming:** Functions `test_*()`, classes `Test*` +- **Small functions:** Prefer composable, testable units over large monoliths + +### Type Hints +- Required on all functions/methods (enforced by mypy config: `disallow_untyped_defs = true`) +- Use modern syntax: `list[str]`, `dict[str, int]` (Python 3.9+) +- Return types always specified: `def foo(...) -> ReturnType:` +- Example valid function: + ```python + def validate_secret_path(path: str) -> bool: + return path.startswith("op://") + ``` + +### Code Style +- **Line length:** 88 characters (Black formatter) +- **Imports:** Sorted by isort (Black profile), auto-fixed by pre-commit +- **Modernization:** pyupgrade auto-converts old syntax (Python 3.12+) +- **Docstrings:** Checked by pydocstyle (Google style recommended) + +### Dependencies +- **Declare in:** `pyproject.toml` only +- **Syntax:** `package>=MAJOR.MINOR.Z, str +# - Use Optional[T] for nullable values: Optional[str] +# - Use Union[T1, T2] for multiple types: Union[int, str] +# - Import from typing: from typing import Optional, Union, Any +``` + +### Debug Pre-Commit Hook Failures +```bash +# Run a specific hook manually +pre-commit run black --all-files +pre-commit run mypy --all-files +pre-commit run bandit --all-files + +# Run all hooks +pre-commit run --all-files + +# See logs if a Python tool failed +# Edit the file to fix the issue, then commit again +``` + +### Load a Secret in Code +```python +from utils.secrets import get_secret + +# Load secret and use directly +api_token = get_secret("op://Engineering/GitHub/token") + +# Or save to environment variable for subprocess +get_secret("op://Engineering/Database/password", "DB_PASSWORD") +# Now can access: os.environ["DB_PASSWORD"] +``` + +### Create a Test +```python +# File: tests/utils/test_my_module.py +from utils.my_module import my_function + +def test_my_function_returns_correct_value() -> None: + result = my_function("input") + assert result == "expected_output" + +def test_my_function_handles_edge_case() -> None: + result = my_function("") + assert result is not None +``` + +### View Test Coverage +```bash +pytest --cov=utils --cov=example --cov-report=html +# Opens: htmlcov/index.html +``` + +--- + +## Common Issues & Solutions + +### โŒ Error: "pytest: command not found" +**Cause:** Virtual environment not activated +**Fix:** +```bash +source .venv/bin/activate # Linux/macOS +.venv\Scripts\Activate.ps1 # Windows +``` + +### โŒ Error: "Pre-commit hook failed" on commit +**Cause:** Code doesn't pass quality checks +**Fix:** +```bash +pre-commit run --all-files # See exactly which checks failed +# Fix the issues (most auto-fix, some require manual edits) +git add . +git commit # Retry +``` + +### โŒ Error: "mypy: disallow_untyped_defs" +**Cause:** Function missing type hints +**Fix:** Add type hints to parameters and return value +```python +# โŒ Before +def process_items(items): + return len(items) + +# โœ… After +def process_items(items: list[str]) -> int: + return len(items) +``` + +### โŒ Error: "Secret detected in file" +**Cause:** detect-secrets hook blocked a secret +**Fix:** +1. Remove the secret from the file +2. Use `get_secret()` instead: + ```python + from utils.secrets import get_secret + api_key = get_secret("op://Vault/Item/field") + ``` +3. Commit again + +### โŒ Error: "op: command not found" +**Cause:** 1Password CLI not installed +**Fix:** See `CONTRIBUTING.md` for 1Password CLI setup +```bash +# Verify installation +op --version +op account list # Authenticate +``` + +### โš ๏ธ Issue: Notebook outputs committed to git +**Cause:** nbstripout hook didn't run (venv likely inactive) +**Fix:** +```bash +# Activate venv +source .venv/bin/activate + +# Strip outputs manually +jupyter nbconvert --ClearingPreprocessor.enabled=True example/using_secrets.ipynb + +# Commit again +``` + +### โš ๏ธ Issue: "Edited uv.lock" shows up in diffs +**Cause:** Manually edited uv.lock (should never happen) +**Fix:** +```bash +# Regenerate correct lock file +uv lock + +# Commit the regenerated version +``` + +--- + +## Pre-Commit Hook Troubleshooting + +See `docs/6_pre_commit_troubleshooting.md` for detailed troubleshooting of individual hooks. + +**Quick reference:** +- **Most hooks auto-fix:** Run `pre-commit run --all-files` to auto-fix, then recommit +- **Some hooks require manual fixes:** mypy, bandit, pydocstyle, detect-secrets +- **Skip all hooks (last resort only):** `git commit --no-verify` +- **Skip config in direnv:** Edit `.envrc` โ†’ run `direnv allow` + +--- + +## File Preferences for AI Coding + +### When Writing Code +1. **Type hints:** Always include themโ€”required for mypy +2. **Docstrings:** Use Google style format; checked by pydocstyle +3. **Functions:** Keep them small and testable (test each function) +4. **Imports:** Use absolute imports, not relative (`from utils ...` not `from ..utils`) +5. **Line length:** Aim for โ‰ค88 characters (Black will format longer lines) + +### Example: Well-Structured Function +```python +"""Module for processing user data.""" + +from typing import Optional +from utils.logging import get_logger + +logger = get_logger(__name__) + + +def validate_email(email: str) -> bool: + """Check if email format is valid. + + Args: + email: Email address to validate. + + Returns: + True if email is valid, False otherwise. + """ + import re + pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" + is_valid = bool(re.match(pattern, email)) + logger.debug(f"Validated email: {is_valid}") + return is_valid +``` + +--- + +## References + +- **README:** Overview and essential rules +- **docs/0_setup.md:** Step-by-step setup +- **docs/1_usage.md:** Daily development guide +- **docs/2_secret_management.md:** 1Password integration +- **docs/3_dependency_management.md:** Using `uv` +- **docs/4_code_quality.md:** Type hints & linting +- **docs/5_pre_commit_hooks.md:** Hook details +- **docs/6_pre_commit_troubleshooting.md:** Troubleshooting +- **CONTRIBUTING.md:** Contribution guidelines +- **pyproject.toml:** Tool configurations (pytest, mypy, ruff, black, etc.) + +--- + +## For AI Assistants (Copilot/Claude) + +When working on code in this repository: + +โœ… **DO:** +- Enforce type hints on all functions (required, not optional) +- Follow the 88-character line limit +- Add comprehensive docstrings (Google style) +- Write tests in `tests/utils/test_*.py` mirroring production structure +- Use `from utils.secrets import get_secret()` for any secrets +- Suggest dependency additions to `pyproject.toml`, not direct pip installs +- Check pre-commit hook configurations for lint/format requirements + +โŒ **DON'T:** +- Write untyped functions (will fail mypy) +- Put secrets or API keys in code +- Use relative imports (`from ..module` instead of `from utils.module`) +- Suggest running `pip install` directly +- Make manual edits to `uv.lock` +- Assume pre-commit hooks should be skipped +- Create imports of unlisted dependencies (always add to `pyproject.toml` first) + +--- + +*Last updated: 2026-03-03* +*For questions or updates, see CONTRIBUTING.md* diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..b07f579a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,47 @@ +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "02:00" + timezone: Europe/Amsterdam + open-pull-requests-limit: 1 + labels: + - dependencies + - github-actions + commit-message: + prefix: deps + include: scope + rebase-strategy: auto + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + day: monday + time: "02:00" + timezone: Europe/Amsterdam + open-pull-requests-limit: 1 + labels: + - dependencies + - python + commit-message: + prefix: deps + include: scope + rebase-strategy: auto + groups: + python-production: + dependency-type: production + patterns: + - "*" + python-development: + dependency-type: development + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c9b0c9a..72a7509f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,10 @@ on: branches: - main - staging + push: + branches: + - main + - staging permissions: contents: write @@ -29,44 +33,43 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Cache pip - uses: actions/cache@v3 + - name: Install uv + uses: astral-sh/setup-uv@v3 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Install pip-tools - run: pip install pip-tools - - - name: Validate lockfiles - run: | - pip-compile --dry-run --generate-hashes \ - --output-file=requirements.txt pyproject.toml - pip-compile --dry-run --generate-hashes --extra=dev \ - --output-file=requirements-dev.txt pyproject.toml + enable-cache: true - name: Install dependencies - run: | - pip install --require-hashes -r requirements.txt - pip install --require-hashes -r requirements-dev.txt + run: uv sync - name: Run pip-audit - run: pip-audit --progress-spinner off -r requirements.txt + run: uv run pip-audit --progress-spinner off --desc --ignore CVE-2026-1703 - name: Black formatting check - run: black --check . + run: uv run black --check . - name: Ruff linting - run: ruff check . + run: uv run ruff check . --fix + + - name: isort import sorting check + run: uv run isort . --check-only --profile=black - name: Type checking with mypy - run: mypy . + run: uv run mypy . --config-file=pyproject.toml + + - name: Run pyupgrade + run: uv run pyupgrade --py312-plus --diff . + + - name: Codespell check + run: uv run codespell . --skip=*.lock + + - name: Bandit security check + run: uv run bandit -r utils example -ll --skip B101 + + - name: Pydocstyle check + run: uv run pydocstyle utils example - name: Run tests with coverage - run: | - pytest --cov --cov-report=term --cov-report=xml + run: uv run pytest --cov --cov-report=term --cov-report=xml - name: Generate coverage badge uses: tj-actions/coverage-badge-py@v2 From 10c78c12a482ec6b826168d9a21660df852ebcc6 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 15:15:55 +0100 Subject: [PATCH 101/199] Refactor dependabot configuration to consolidate updates into multi-ecosystem groups and improve scheduling settings --- .github/dependabot.yml | 64 +++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b07f579a..95af84f1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,47 +1,29 @@ version: 2 -updates: - - package-ecosystem: github-actions - directory: / +multi-ecosystem-groups: + all-updates: schedule: - interval: weekly - day: monday - time: "02:00" - timezone: Europe/Amsterdam - open-pull-requests-limit: 1 + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "Europe/Amsterdam" labels: - - dependencies - - github-actions + - "dependencies" + - "dependabot" commit-message: - prefix: deps - include: scope - rebase-strategy: auto - groups: - github-actions: - patterns: - - "*" + prefix: "deps" + include: "scope" + target-branch: "main" - - package-ecosystem: pip - directory: / - schedule: - interval: weekly - day: monday - time: "02:00" - timezone: Europe/Amsterdam - open-pull-requests-limit: 1 - labels: - - dependencies - - python - commit-message: - prefix: deps - include: scope - rebase-strategy: auto - groups: - python-production: - dependency-type: production - patterns: - - "*" - python-development: - dependency-type: development - patterns: - - "*" +updates: + - package-ecosystem: "github-actions" + directory: "/" + multi-ecosystem-group: "all-updates" + patterns: + - "*" + + - package-ecosystem: "uv" + directory: "/" + multi-ecosystem-group: "all-updates" + patterns: + - "*" From e97b84e9dc2d969a94599ef803151b04adc0915e Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 15:24:46 +0100 Subject: [PATCH 102/199] Add helper script to integrate pip-audit ignore list from pyproject.toml This commit introduces a new script, run-pip-audit.sh, which extracts the CVE exclusion list from the pyproject.toml file under the [tool.pip-audit] section. This ensures that both pre-commit hooks and CI processes utilize a consistent set of vulnerabilities to ignore when running pip-audit. The script handles errors in reading the configuration file gracefully. --- .github/copilot-instructions.md | 17 ++ .github/workflows/ci.yml | 2 +- .pre-commit-config.yaml | 4 +- docs/1_usage.md | 43 +++ docs/6_pre_commit_troubleshooting.md | 63 +++- pyproject.toml | 12 +- scripts/run-pip-audit.sh | 27 ++ uv.lock | 421 +++++++++++++++------------ 8 files changed, 388 insertions(+), 201 deletions(-) create mode 100644 scripts/run-pip-audit.sh diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fa35a50e..c2122b4e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -232,6 +232,23 @@ uv run mypy . --config-file=pyproject.toml # - Import from typing: from typing import Optional, Union, Any ``` +### Manage CVE Exclusions for pip-audit +```bash +# Edit pyproject.toml [tool.pip-audit] section to add/remove exclusions +# Both pre-commit and CI will automatically use the updated list + +[tool.pip-audit] +ignore = [ + "CVE-2026-1703", # Brief explanation of why this CVE is excluded + "CVE-2024-XXXXX", # Add new exclusions here +] + +# Run pip-audit locally to verify +uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc + +# Script automatically reads from pyproject.toml and applies exclusions +``` + ### Debug Pre-Commit Hook Failures ```bash # Run a specific hook manually diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72a7509f..7485234a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: run: uv sync - name: Run pip-audit - run: uv run pip-audit --progress-spinner off --desc --ignore CVE-2026-1703 + run: uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc - name: Black formatting check run: uv run black --check . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc6a89cb..c41099de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,9 @@ repos: rev: v2.10.0 hooks: - id: pip-audit - args: ["--progress-spinner", "off", "--desc", "--ignore", "CVE-2026-1703"] + args: ["--progress-spinner", "off", "--desc"] + entry: uv run bash scripts/run-pip-audit.sh + language: system stages: [pre-commit] # Secrets scanning diff --git a/docs/1_usage.md b/docs/1_usage.md index 17c9398b..40f0a57a 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -63,6 +63,49 @@ uv sync Always use version constraints like `>=X.Y.Z, /dev/null ### pip-audit (Dependency Vulnerabilities) -**Problem:** Known vulnerability that's not fixable +#### Adding a CVE Exclusion -**Solutions:** -```bash -# Ignore specific CVE (temporary) -- id: pip-audit - args: ["--ignore", "CVE-2026-1234"] +**Problem:** A vulnerability is reported but you need to exclude it (false positive, known mitigated risk, or fix not available). -# Or update dependencies -uv lock --upgrade -uv sync +**Solution:** Add to `pyproject.toml` in the centralized `[tool.pip-audit]` section: + +```toml +[tool.pip-audit] +# Centralized CVE/PYSEC exclusions for pip-audit +# Used by both pre-commit hooks and CI workflows via scripts/run-pip-audit.sh +ignore = [ + "CVE-2026-1703", # Path traversal in pip (only affects pip extraction) + "CVE-2024-XXXXX", # Your new exclusion with explanation +] ``` -**Problem:** "pyright", "autopep8" or old packages show vulnerabilities +**Workflow:** +1. Run pip-audit to see the vulnerability: + ```bash + uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc + ``` +2. Identify the CVE/PYSEC ID from the output +3. Add the ID to the `ignore` list in `pyproject.toml` +4. Document why it's excluded (false positive, mitigated, etc.) +5. Both pre-commit (local) and CI (GitHub Actions) will automatically pick up the change + +**Important:** +- All exclusions go in **ONE place**: `pyproject.toml [tool.pip-audit]` +- The `scripts/run-pip-audit.sh` wrapper reads from `pyproject.toml` and applies the exclusions +- Never add `--ignore-vuln` directly to pre-commit config or CI workflowโ€”it goes out of sync +- Commit changes to `pyproject.toml` like any other code change + +#### Updating Dependencies to Fix Vulnerabilities -**Solution:** Review dependencies in `pyproject.toml`: +**Problem:** Vulnerability has a fix available + +**Solution:** Upgrade the vulnerable package: ```bash # Check which package needs updating uv pip show -# Update if available +# Update the dependency uv lock --upgrade-package +uv sync + +# Run pip-audit to verify +uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc ``` +#### Verifying Exclusions Work + +**Test locally:** +```bash +# Run pip-audit with exclusions from pyproject.toml +uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc + +# Or run pre-commit manually +pre-commit run pip-audit --all-files +``` + +**Test in CI:** +Push changes to a branch โ†’ GitHub will run CI and show if pip-audit passes + --- ## Docstring Validation diff --git a/pyproject.toml b/pyproject.toml index 72b10a3b..015800b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,10 +32,10 @@ classifiers = [ # Pinned with >= and < to allow patch/minor updates while preventing major version breaks. # Add only dependencies that are needed for the app to run in production. dependencies = [ - "flask>=3.1.0,<4.0.0", # Example dependency, replace with your own + "flask>=3.1.3,<4.0.0", # Example dependency, replace with your own "requests>=2.32.0,<3.0.0", "urllib3>=2.6.0,<3.0.0", - "werkzeug>=3.1.0,<4.0.0", + "werkzeug>=3.1.6,<4.0.0", "filelock>=3.20.0,<4.0.0", ] @@ -190,3 +190,11 @@ lint.ignore = [ [tool.bandit] exclude_dirs = ["tests", "venv"] skips = ["B101", "B601"] + +[tool.pip-audit] +# Centralized CVE/PYSEC exclusions for pip-audit +# Used by both pre-commit hooks and CI workflows via scripts/run-pip-audit.sh +# See docs/6_pre_commit_troubleshooting.md for explanation of each exclusion +ignore = [ + "CVE-2026-1703", # Path traversal in pip when extracting malicious wheels +] diff --git a/scripts/run-pip-audit.sh b/scripts/run-pip-audit.sh new file mode 100644 index 00000000..0202b037 --- /dev/null +++ b/scripts/run-pip-audit.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Helper script to build pip-audit ignore arguments from pyproject.toml [tool.pip-audit] +# This ensures both pre-commit and CI use the same CVE exclusion list + +# Extract ignore list from pyproject.toml using Python +IGNORE_ARGS=$(python3 << 'EOF' +import tomllib +import sys + +try: + with open("pyproject.toml", "rb") as f: + config = tomllib.load(f) + + # Get the ignore list from [tool.pip-audit] + ignore_list = config.get("tool", {}).get("pip-audit", {}).get("ignore", []) + + # Build the arguments + args = " ".join([f"--ignore-vuln {cve}" for cve in ignore_list]) + print(args) +except Exception as e: + print(f"Error reading pyproject.toml: {e}", file=sys.stderr) + sys.exit(1) +EOF +) + +# Run pip-audit with the exclusions from pyproject.toml +pip-audit $IGNORE_ARGS "$@" diff --git a/uv.lock b/uv.lock index 333e1fe3..28154a2f 100644 --- a/uv.lock +++ b/uv.lock @@ -105,11 +105,11 @@ filecache = [ [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] @@ -210,76 +210,86 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, - { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, - { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, - { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, - { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, - { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, - { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, - { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, - { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, - { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, - { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, - { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [[package]] @@ -340,16 +350,16 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, ] [[package]] name = "flask" -version = "3.1.2" +version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, @@ -359,18 +369,18 @@ dependencies = [ { name = "markupsafe" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] [[package]] name = "identify" -version = "2.6.16" +version = "2.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, + { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, ] [[package]] @@ -463,54 +473,62 @@ wheels = [ [[package]] name = "librt" -version = "0.7.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, - { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, - { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, - { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, - { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, - { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, - { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, - { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, - { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, - { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, - { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, - { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, - { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, - { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, - { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, - { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, - { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, - { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, - { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, - { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, - { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, - { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, - { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, - { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, - { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, - { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, - { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, - { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, - { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, - { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, - { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, - { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] [[package]] @@ -712,14 +730,14 @@ wheels = [ [[package]] name = "nbstripout" -version = "0.8.2" +version = "0.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nbformat" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/09/cea5360a0788f94337d1b65c313ff0a448fb6a444b65cab89378eb8f7d5c/nbstripout-0.8.2.tar.gz", hash = "sha256:2876530eb684bf93a5b48fe6d92b2163f78d040721c76b37d5b9e1514d38fc69", size = 27747, upload-time = "2025-11-16T17:38:55.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/6f/b52c4da26babeb521078c08c78c3187a59197098ffc7a70b0fe76851813a/nbstripout-0.9.1.tar.gz", hash = "sha256:313bbb4217c8e38998567e5d790b6bd6c3a17a8c39073b205b84dadfc5d756dc", size = 32356, upload-time = "2026-02-21T16:19:55.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.py3-none-any.whl", hash = "sha256:5f06f9138cb64daed3e91c5359ff0fff85bab4d0db7d72723be1da6f51b890ae", size = 17122, upload-time = "2025-11-16T17:38:54.164Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl", hash = "sha256:ca027ee45742ee77e4f8e9080254f9a707f1161ba11367b82fdf4a29892c759e", size = 19136, upload-time = "2026-02-21T16:19:54.868Z" }, ] [[package]] @@ -742,29 +760,29 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.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" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pathspec" -version = "1.0.3" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "pip" -version = "25.3" +version = "26.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014, upload-time = "2025-10-25T00:55:41.394Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, ] [[package]] @@ -815,11 +833,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] @@ -882,11 +900,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.3.1" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -968,10 +986,10 @@ dev = [ [package.metadata] requires-dist = [ { name = "filelock", specifier = ">=3.20.0,<4.0.0" }, - { name = "flask", specifier = ">=3.1.0,<4.0.0" }, + { name = "flask", specifier = ">=3.1.3,<4.0.0" }, { name = "requests", specifier = ">=2.32.0,<3.0.0" }, { name = "urllib3", specifier = ">=2.6.0,<3.0.0" }, - { name = "werkzeug", specifier = ">=3.1.0,<4.0.0" }, + { name = "werkzeug", specifier = ">=3.1.6,<4.0.0" }, ] [package.metadata.requires-dev] @@ -997,12 +1015,45 @@ dev = [ extra = [] [[package]] -name = "pytokens" -version = "0.3.0" +name = "python-discovery" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] [[package]] @@ -1094,15 +1145,15 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] @@ -1188,28 +1239,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, - { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, - { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, - { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, - { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, - { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, - { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, - { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, - { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, - { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, - { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, - { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, +version = "0.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" }, + { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" }, + { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" }, + { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, ] [[package]] @@ -1331,26 +1381,27 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.36.1" +version = "21.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, + { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, + { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, ] [[package]] name = "werkzeug" -version = "3.1.5" +version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, ] From 6eed6c6ff2b64c5c297dd2ed1c152b9854ace7fc Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 15:27:00 +0100 Subject: [PATCH 103/199] Refactor CI workflow to remove individual checks and consolidate into pre-commit hooks --- .github/workflows/ci.yml | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7485234a..70c67b4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,32 +41,8 @@ jobs: - name: Install dependencies run: uv sync - - name: Run pip-audit - run: uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc - - - name: Black formatting check - run: uv run black --check . - - - name: Ruff linting - run: uv run ruff check . --fix - - - name: isort import sorting check - run: uv run isort . --check-only --profile=black - - - name: Type checking with mypy - run: uv run mypy . --config-file=pyproject.toml - - - name: Run pyupgrade - run: uv run pyupgrade --py312-plus --diff . - - - name: Codespell check - run: uv run codespell . --skip=*.lock - - - name: Bandit security check - run: uv run bandit -r utils example -ll --skip B101 - - - name: Pydocstyle check - run: uv run pydocstyle utils example + - name: Run pre-commit hooks + run: pre-commit run --all-files - name: Run tests with coverage run: uv run pytest --cov --cov-report=term --cov-report=xml From f88d5ee4469650704154f1b2e80db760a5782fcc Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 15:27:38 +0100 Subject: [PATCH 104/199] Fix CI workflow to use 'uv run' for pre-commit hooks execution --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70c67b4d..ff265644 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: run: uv sync - name: Run pre-commit hooks - run: pre-commit run --all-files + run: uv run pre-commit run --all-files - name: Run tests with coverage run: uv run pytest --cov --cov-report=term --cov-report=xml From 621a341d71df2ae458370221a91cf2cd5a99fb99 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 15:30:00 +0100 Subject: [PATCH 105/199] Update CI workflow to use latest action versions and improve pre-commit caching --- .github/workflows/ci.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff265644..21a94bf6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,23 +24,32 @@ jobs: python-version: ['3.12'] steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: true - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v5 with: enable-cache: true + cache-dependency-glob: "uv.lock" - name: Install dependencies run: uv sync + - name: Cache pre-commit hooks + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + pre-commit-${{ runner.os }}-${{ matrix.python-version }}- + - name: Run pre-commit hooks run: uv run pre-commit run --all-files From 0fcccf64a0664af6ba53c5619655515fef70e422 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 15:32:56 +0100 Subject: [PATCH 106/199] Update coverage badge and add genbadge dependency - Updated the coverage badge SVG to improve accessibility and design. - Added `genbadge[coverage]` version 1.1.0 to the development dependencies in `pyproject.toml`. - Updated `uv.lock` to include `genbadge` and its dependencies. - Added optional dependencies for `genbadge` in `uv.lock`. - Updated `setuptools` to version 82.0.0 in `uv.lock`. --- .github/workflows/ci.yml | 6 +-- coverage.svg | 22 +-------- pyproject.toml | 1 + uv.lock | 100 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21a94bf6..7a4de572 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,11 +57,7 @@ jobs: run: uv run pytest --cov --cov-report=term --cov-report=xml - name: Generate coverage badge - uses: tj-actions/coverage-badge-py@v2 - with: - working-directory: . - output: coverage.svg - overwrite: true + run: uv run genbadge coverage -i coverage.xml -o coverage.svg - name: Commit badge uses: stefanzweifel/git-auto-commit-action@v5 diff --git a/coverage.svg b/coverage.svg index e5db27cd..a00822ea 100644 --- a/coverage.svg +++ b/coverage.svg @@ -1,21 +1 @@ - - - - - - - - - - - - - - - - coverage - coverage - 100% - 100% - - +coverage: 100.00%coverage100.00% diff --git a/pyproject.toml b/pyproject.toml index 015800b2..3f7289c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ dev = [ "codespell>=2.2.0,<3.0.0", "bandit>=1.7.0,<2.0.0", "pydocstyle>=6.3.0,<7.0.0", + "genbadge[coverage]>=1.1.0,<2.0.0", ] [tool.setuptools.packages.find] diff --git a/uv.lock b/uv.lock index 28154a2f..f1adcc41 100644 --- a/uv.lock +++ b/uv.lock @@ -374,6 +374,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] +[[package]] +name = "genbadge" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pillow" }, + { name = "requests" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/08/686a720bd9f407a2b689c50a94e53b2d26f6ddc6f921ae45ec15c401ee67/genbadge-1.1.3.tar.gz", hash = "sha256:2292ea9cc20af4463dfde952c6b15544fdab9d6e50945f63a42cc400c521fa74", size = 138264, upload-time = "2025-11-24T14:55:01.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/cc/e67b1fe7a9d76a316e9149855a953c37c463caf1e351b1a0abf7f2fb9e38/genbadge-1.1.3-py2.py3-none-any.whl", hash = "sha256:6e4316c171c6f0f84becae4eb116258340bdc054458632abc622d36b8040655e", size = 101262, upload-time = "2025-11-24T14:54:59.925Z" }, +] + +[package.optional-dependencies] +coverage = [ + { name = "defusedxml" }, +] + [[package]] name = "identify" version = "2.6.17" @@ -776,6 +796,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, +] + [[package]] name = "pip" version = "26.0.1" @@ -969,6 +1058,7 @@ dev = [ { name = "black" }, { name = "codespell" }, { name = "detect-secrets" }, + { name = "genbadge", extra = ["coverage"] }, { name = "isort" }, { name = "mypy" }, { name = "nbstripout" }, @@ -999,6 +1089,7 @@ dev = [ { name = "black", specifier = ">=25.12.0,<26.0.0" }, { name = "codespell", specifier = ">=2.2.0,<3.0.0" }, { name = "detect-secrets", specifier = ">=1.5.0,<2.0.0" }, + { name = "genbadge", extras = ["coverage"], specifier = ">=1.1.0,<2.0.0" }, { name = "isort", specifier = ">=5.13.0,<6.0.0" }, { name = "mypy", specifier = ">=1.19.0,<2.0.0" }, { name = "nbstripout", specifier = ">=0.8.0,<1.0.0" }, @@ -1262,6 +1353,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, ] +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + [[package]] name = "snowballstemmer" version = "3.0.1" From d388497036eacfd039805f07d8cddf0ea385fa87 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 15:36:20 +0100 Subject: [PATCH 107/199] Fix CI workflow to conditionally commit coverage badge on pull requests --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a4de572..9ac9cbc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,9 @@ jobs: run: uv run genbadge coverage -i coverage.xml -o coverage.svg - name: Commit badge + if: github.event_name == 'pull_request' uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: "chore: update coverage badge" file_pattern: coverage.svg + branch: ${{ github.head_ref }} From 74f36977feacba54b6ec8fbb52c0c0a665721cd3 Mon Sep 17 00:00:00 2001 From: thom-techlete <176420111+thom-techlete@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:38:11 +0000 Subject: [PATCH 108/199] chore: update coverage badge --- coverage.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage.svg b/coverage.svg index a00822ea..ef52bd22 100644 --- a/coverage.svg +++ b/coverage.svg @@ -1 +1 @@ -coverage: 100.00%coverage100.00% +coverage: 100.00%coverage100.00% \ No newline at end of file From cfd2d12636f32e12b2789242872c6114a8e25cfa Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 15:44:08 +0100 Subject: [PATCH 109/199] Update CI workflow to use custom pre-commit cache path and exclude coverage.svg from checks --- .github/workflows/ci.yml | 4 +++- .pre-commit-config.yaml | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ac9cbc0..668e1cf9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,8 @@ jobs: lint-and-test: name: Lint & Test runs-on: ubuntu-latest + env: + PRE_COMMIT_HOME: ${{ github.workspace }}/.pre-commit-cache strategy: matrix: python-version: ['3.12'] @@ -45,7 +47,7 @@ jobs: - name: Cache pre-commit hooks uses: actions/cache@v4 with: - path: ~/.cache/pre-commit + path: ${{ github.workspace }}/.pre-commit-cache key: pre-commit-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} restore-keys: | pre-commit-${{ runner.os }}-${{ matrix.python-version }}- diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c41099de..f862c1c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,7 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer + exclude: coverage\.svg$ - id: check-toml - id: check-json - id: check-symlinks From c4bbd397caa4bc05416eace1380f8007994df2fb Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 3 Mar 2026 15:50:17 +0100 Subject: [PATCH 110/199] Add required-version for uv tool in pyproject.toml --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3f7289c9..62e9c2ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -199,3 +199,6 @@ skips = ["B101", "B601"] ignore = [ "CVE-2026-1703", # Path traversal in pip when extracting malicious wheels ] + +[tool.uv] +required-version = ">=0.9.25,<1.0.0" From f8cb7cd9850c61fe6c233acf124343466e012c68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:19:15 +0000 Subject: [PATCH 111/199] deps(deps): bump the all-updates group with 4 updates Bumps the all-updates group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action). Updates `actions/checkout` from 3 to 6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v6) Updates `actions/setup-python` from 4 to 6 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v6) Updates `astral-sh/setup-uv` from 3 to 7 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v7) Updates `stefanzweifel/git-auto-commit-action` from 5 to 7 - [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases) - [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v5...v7) deps(deps): bump the all-updates group with 8 updates Bumps the all-updates group with 8 updates: | Package | From | To | | --- | --- | --- | | [flask](https://github.com/pallets/flask) | `3.1.2` | `3.1.3` | | [werkzeug](https://github.com/pallets/werkzeug) | `3.1.5` | `3.1.6` | | [filelock](https://github.com/tox-dev/py-filelock) | `3.20.3` | `3.25.0` | | [nbstripout](https://github.com/kynan/nbstripout) | `0.8.2` | `0.9.1` | | [black](https://github.com/psf/black) | `25.12.0` | `26.1.0` | | [ruff](https://github.com/astral-sh/ruff) | `0.14.11` | `0.15.4` | | [rich](https://github.com/Textualize/rich) | `14.2.0` | `14.3.3` | | [isort](https://github.com/PyCQA/isort) | `5.13.2` | `8.0.1` | Updates `flask` from 3.1.2 to 3.1.3 - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3) Updates `werkzeug` from 3.1.5 to 3.1.6 - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/werkzeug/compare/3.1.5...3.1.6) Updates `filelock` from 3.20.3 to 3.25.0 - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/py-filelock/compare/3.20.3...3.25.0) Updates `nbstripout` from 0.8.2 to 0.9.1 - [Release notes](https://github.com/kynan/nbstripout/releases) - [Changelog](https://github.com/kynan/nbstripout/blob/main/CHANGELOG.md) - [Commits](https://github.com/kynan/nbstripout/compare/0.8.2...0.9.1) Updates `black` from 25.12.0 to 26.1.0 - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/25.12.0...26.1.0) Updates `ruff` from 0.14.11 to 0.15.4 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.11...0.15.4) Updates `rich` from 14.2.0 to 14.3.3 - [Release notes](https://github.com/Textualize/rich/releases) - [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md) - [Commits](https://github.com/Textualize/rich/compare/v14.2.0...v14.3.3) Updates `isort` from 5.13.2 to 8.0.1 - [Release notes](https://github.com/PyCQA/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md) - [Commits](https://github.com/PyCQA/isort/compare/5.13.2...8.0.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-updates - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-updates - dependency-name: astral-sh/setup-uv dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-updates - dependency-name: stefanzweifel/git-auto-commit-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-updates - dependency-name: flask dependency-version: 3.1.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: werkzeug dependency-version: 3.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-updates - dependency-name: filelock dependency-version: 3.25.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: nbstripout dependency-version: 0.9.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: black dependency-version: 26.1.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: all-updates - dependency-name: ruff dependency-version: 0.15.4 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: rich dependency-version: 14.3.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-updates - dependency-name: isort dependency-version: 8.0.1 dependency-type: direct:development update-type: version-update:semver-major dependency-group: all-updates ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 9 ++++---- pyproject.toml | 4 ++-- uv.lock | 46 ++++++++++++++++++++-------------------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 668e1cf9..8e14abf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,17 +26,17 @@ jobs: python-version: ['3.12'] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: true - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "uv.lock" @@ -62,8 +62,7 @@ jobs: run: uv run genbadge coverage -i coverage.xml -o coverage.svg - name: Commit badge - if: github.event_name == 'pull_request' - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: "chore: update coverage badge" file_pattern: coverage.svg diff --git a/pyproject.toml b/pyproject.toml index 62e9c2ac..8d909657 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,14 +59,14 @@ dev = [ "nbstripout>=0.8.0,<1.0.0", "pytest>=9.0.0,<10.0.0", "pytest-cov>=7.0.0,<8.0.0", - "black>=25.12.0,<26.0.0", + "black>=25.12.0,<27.0.0", "mypy>=1.19.0,<2.0.0", "ruff>=0.14.0,<1.0.0", "beartype>=0.22.0,<1.0.0", "pytest-beartype>=0.2.0,<1.0.0", "pip-audit>=2.10.0,<3.0.0", "rich>=14.2.0,<15.0.0", - "isort>=5.13.0,<6.0.0", + "isort>=5.13.0,<9.0.0", "pyupgrade>=3.15.0,<4.0.0", "codespell>=2.2.0,<3.0.0", "bandit>=1.7.0,<2.0.0", diff --git a/uv.lock b/uv.lock index f1adcc41..c20b769e 100644 --- a/uv.lock +++ b/uv.lock @@ -37,7 +37,7 @@ wheels = [ [[package]] name = "black" -version = "25.12.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -47,24 +47,24 @@ dependencies = [ { name = "platformdirs" }, { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" }, - { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" }, - { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" }, - { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" }, - { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" }, - { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" }, - { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" }, - { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" }, - { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" }, - { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, ] [[package]] @@ -423,11 +423,11 @@ wheels = [ [[package]] name = "isort" -version = "5.13.2" +version = "8.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, ] [[package]] @@ -1086,11 +1086,11 @@ requires-dist = [ dev = [ { name = "bandit", specifier = ">=1.7.0,<2.0.0" }, { name = "beartype", specifier = ">=0.22.0,<1.0.0" }, - { name = "black", specifier = ">=25.12.0,<26.0.0" }, + { name = "black", specifier = ">=25.12.0,<27.0.0" }, { name = "codespell", specifier = ">=2.2.0,<3.0.0" }, { name = "detect-secrets", specifier = ">=1.5.0,<2.0.0" }, { name = "genbadge", extras = ["coverage"], specifier = ">=1.1.0,<2.0.0" }, - { name = "isort", specifier = ">=5.13.0,<6.0.0" }, + { name = "isort", specifier = ">=5.13.0,<9.0.0" }, { name = "mypy", specifier = ">=1.19.0,<2.0.0" }, { name = "nbstripout", specifier = ">=0.8.0,<1.0.0" }, { name = "pip-audit", specifier = ">=2.10.0,<3.0.0" }, From 6d80b556b2a90b90b298c815253222167be309d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:15:39 +0000 Subject: [PATCH 112/199] deps(deps-dev): bump the all-updates group across 1 directory with 2 updates Bumps the all-updates group with 2 updates in the / directory: [black](https://github.com/psf/black) and [isort](https://github.com/PyCQA/isort). Updates `black` from 25.12.0 to 26.1.0 - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/25.12.0...26.1.0) Updates `isort` from 5.13.2 to 8.0.1 - [Release notes](https://github.com/PyCQA/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md) - [Commits](https://github.com/PyCQA/isort/compare/5.13.2...8.0.1) --- updated-dependencies: - dependency-name: black dependency-version: 26.1.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: all-updates - dependency-name: isort dependency-version: 8.0.1 dependency-type: direct:development update-type: version-update:semver-major dependency-group: all-updates ... Signed-off-by: dependabot[bot] --- pyproject.toml | 4 ++-- uv.lock | 46 +++++++++++++++++++++++----------------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 015800b2..70728e4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,14 +59,14 @@ dev = [ "nbstripout>=0.8.0,<1.0.0", "pytest>=9.0.0,<10.0.0", "pytest-cov>=7.0.0,<8.0.0", - "black>=25.12.0,<26.0.0", + "black>=25.12.0,<27.0.0", "mypy>=1.19.0,<2.0.0", "ruff>=0.14.0,<1.0.0", "beartype>=0.22.0,<1.0.0", "pytest-beartype>=0.2.0,<1.0.0", "pip-audit>=2.10.0,<3.0.0", "rich>=14.2.0,<15.0.0", - "isort>=5.13.0,<6.0.0", + "isort>=5.13.0,<9.0.0", "pyupgrade>=3.15.0,<4.0.0", "codespell>=2.2.0,<3.0.0", "bandit>=1.7.0,<2.0.0", diff --git a/uv.lock b/uv.lock index 28154a2f..0b809272 100644 --- a/uv.lock +++ b/uv.lock @@ -37,7 +37,7 @@ wheels = [ [[package]] name = "black" -version = "25.12.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -47,24 +47,24 @@ dependencies = [ { name = "platformdirs" }, { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" }, - { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" }, - { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" }, - { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" }, - { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" }, - { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" }, - { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" }, - { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" }, - { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" }, - { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, ] [[package]] @@ -403,11 +403,11 @@ wheels = [ [[package]] name = "isort" -version = "5.13.2" +version = "8.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, ] [[package]] @@ -996,10 +996,10 @@ requires-dist = [ dev = [ { name = "bandit", specifier = ">=1.7.0,<2.0.0" }, { name = "beartype", specifier = ">=0.22.0,<1.0.0" }, - { name = "black", specifier = ">=25.12.0,<26.0.0" }, + { name = "black", specifier = ">=25.12.0,<27.0.0" }, { name = "codespell", specifier = ">=2.2.0,<3.0.0" }, { name = "detect-secrets", specifier = ">=1.5.0,<2.0.0" }, - { name = "isort", specifier = ">=5.13.0,<6.0.0" }, + { name = "isort", specifier = ">=5.13.0,<9.0.0" }, { name = "mypy", specifier = ">=1.19.0,<2.0.0" }, { name = "nbstripout", specifier = ">=0.8.0,<1.0.0" }, { name = "pip-audit", specifier = ">=2.10.0,<3.0.0" }, From b774c82ec353a9d0c81f1d138ffc1d0d5c72433a Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 4 Mar 2026 11:49:47 +0100 Subject: [PATCH 113/199] refactor: clean up pyproject.toml formatting and reorganize dependencies --- pyproject.toml | 136 +++++++++++++++++++++++-------------------------- 1 file changed, 64 insertions(+), 72 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8d909657..ea419e12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,25 +14,21 @@ description = "A basic Python project template with full DevOps tooling support. readme = "README.md" requires-python = ">=3.12,<4.0" keywords = ["devops", "template", "python"] -authors = [ - {name = "Thom Ezechiels", email = "thom@techletes.ai"}, -] -maintainers = [ - { name = "Techletes DevOps Team", email = "dev@techletes.ai" } -] +authors = [{ name = "Thom Ezechiels", email = "thom@techletes.ai" }] +maintainers = [{ name = "Techletes DevOps Team", email = "dev@techletes.ai" }] classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", ] # Core production dependencies required for the application to run. # Pinned with >= and < to allow patch/minor updates while preventing major version breaks. # Add only dependencies that are needed for the app to run in production. dependencies = [ - "flask>=3.1.3,<4.0.0", # Example dependency, replace with your own + "flask>=3.1.3,<4.0.0", # Example dependency, replace with your own "requests>=2.32.0,<3.0.0", "urllib3>=2.6.0,<3.0.0", "werkzeug>=3.1.6,<4.0.0", @@ -43,6 +39,29 @@ dependencies = [ # Managed by uv (PEP 735 format) and locked in uv.lock for reproducible builds. # Install with: uv sync (all groups), uv sync --no-group dev (no dev), uv sync --only-group extra (extra only), etc. [dependency-groups] +# Development and testing dependencies. +# Includes code quality tools, testing frameworks, and development utilities. +# Install with: uv sync (default includes dev) or uv sync --no-group dev to skip +dev = [ + "pre-commit>=4.5.0,<5.0.0", + "detect-secrets>=1.5.0,<2.0.0", + "nbstripout>=0.8.0,<1.0.0", + "pytest>=9.0.0,<10.0.0", + "pytest-cov>=7.0.0,<8.0.0", + "black>=25.12.0,<27.0.0", + "mypy>=1.19.0,<2.0.0", + "ruff>=0.14.0,<1.0.0", + "beartype>=0.22.0,<1.0.0", + "pytest-beartype>=0.2.0,<1.0.0", + "pip-audit>=2.10.0,<3.0.0", + "rich>=14.2.0,<15.0.0", + "isort>=5.13.0,<9.0.0", + "pyupgrade>=3.15.0,<4.0.0", + "codespell>=2.2.0,<3.0.0", + "bandit>=1.7.0,<2.0.0", + "pydocstyle>=6.3.0,<7.0.0", + "genbadge[coverage]>=1.1.0,<2.0.0", +] # Additional production features and optional components. # Install with: uv sync --group extra or uv sync --no-group extra @@ -50,29 +69,6 @@ extra = [ # "zero-friction @ git+https://github.com/TECHLETES/zero_friction.git@main" ] -# Development and testing dependencies. -# Includes code quality tools, testing frameworks, and development utilities. -# Install with: uv sync (default includes dev) or uv sync --no-group dev to skip -dev = [ - "pre-commit>=4.5.0,<5.0.0", - "detect-secrets>=1.5.0,<2.0.0", - "nbstripout>=0.8.0,<1.0.0", - "pytest>=9.0.0,<10.0.0", - "pytest-cov>=7.0.0,<8.0.0", - "black>=25.12.0,<27.0.0", - "mypy>=1.19.0,<2.0.0", - "ruff>=0.14.0,<1.0.0", - "beartype>=0.22.0,<1.0.0", - "pytest-beartype>=0.2.0,<1.0.0", - "pip-audit>=2.10.0,<3.0.0", - "rich>=14.2.0,<15.0.0", - "isort>=5.13.0,<9.0.0", - "pyupgrade>=3.15.0,<4.0.0", - "codespell>=2.2.0,<3.0.0", - "bandit>=1.7.0,<2.0.0", - "pydocstyle>=6.3.0,<7.0.0", - "genbadge[coverage]>=1.1.0,<2.0.0", -] [tool.setuptools.packages.find] where = ["."] @@ -86,14 +82,14 @@ target-version = ["py312"] [tool.pytest] minversion = "9.0" addopts = [ - "--strict-markers", - "--strict-config", - "--cov=utils", - "--cov=example", - "--cov-report=term-missing", - "--cov-report=html", - "--cov-report=xml", - "--beartype-packages=utils,example", + "--strict-markers", + "--strict-config", + "--cov=utils", + "--cov=example", + "--cov-report=term-missing", + "--cov-report=html", + "--cov-report=xml", + "--beartype-packages=utils,example", ] testpaths = ["tests"] python_files = ["test_*.py", "*_test.py"] @@ -104,30 +100,30 @@ pythonpath = ["."] [tool.coverage.run] source = ["utils", "example"] omit = [ - "*/__pycache__/*", - "*/tests/*", - "*/example/*", - "*/.venv/*", - "*/venv/*", - "*/__init__.py", - "setup.py", - # Add any other files you want to exclude from test coverage + "*/__pycache__/*", + "*/tests/*", + "*/example/*", + "*/.venv/*", + "*/venv/*", + "*/__init__.py", + "setup.py", + # Add any other files you want to exclude from test coverage ] [tool.coverage.report] exclude_lines = [ - "pragma: no cover", - "def __repr__", - "if self.debug:", - "if settings.DEBUG", - "raise AssertionError", - "raise NotImplementedError", - "if 0:", - "if __name__ == .__main__.:", - "class .*\\bProtocol\\):", - "@(abc\\.)?abstractmethod", - "except ImportError", - "pass", + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", + "except ImportError", + "pass", ] [tool.mypy] @@ -151,11 +147,7 @@ warn_unused_configs = true disallow_untyped_defs = true disallow_incomplete_defs = true -exclude = [ - "migrations", - "notebooks", - "tests", -] +exclude = ["migrations", "notebooks", "tests"] plugins = [] # Loosen rules for test and script files @@ -166,7 +158,7 @@ disallow_incomplete_defs = false warn_return_any = false [tool.ruff] -target-version = "py311" +target-version = "py312" line-length = 88 fix = true respect-gitignore = true @@ -184,8 +176,8 @@ lint.select = [ # Rules to ignore (common ones) lint.ignore = [ - "B008", # Do not perform function calls in argument defaults - "E501", # Line too long (Black handles this) + "B008", # Do not perform function calls in argument defaults + "E501", # Line too long (Black handles this) ] [tool.bandit] @@ -197,7 +189,7 @@ skips = ["B101", "B601"] # Used by both pre-commit hooks and CI workflows via scripts/run-pip-audit.sh # See docs/6_pre_commit_troubleshooting.md for explanation of each exclusion ignore = [ - "CVE-2026-1703", # Path traversal in pip when extracting malicious wheels + "CVE-2026-1703", # Path traversal in pip when extracting malicious wheels ] [tool.uv] From fd602e714034e17b247931573cb432854b3e01a9 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 4 Mar 2026 12:08:27 +0100 Subject: [PATCH 114/199] fix: add conditional check for pull request in coverage badge generation --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e14abf2..c9d9f433 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,10 +59,12 @@ jobs: run: uv run pytest --cov --cov-report=term --cov-report=xml - name: Generate coverage badge + if: github.event_name == 'pull_request' run: uv run genbadge coverage -i coverage.xml -o coverage.svg - name: Commit badge uses: stefanzweifel/git-auto-commit-action@v7 + if: github.event_name == 'pull_request' with: commit_message: "chore: update coverage badge" file_pattern: coverage.svg From 7ade214be460083adc04e6a233abd054e722f93b Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 13 Apr 2026 16:16:06 +0200 Subject: [PATCH 115/199] feat: add devcontainer setup with Python and uv (#28) - Create Dockerfile for devcontainer using Python 3.12 and install uv. - Add devcontainer.json for configuration, including workspace settings and VS Code extensions. - Implement post-create.sh for workspace bootstrapping and pre-commit hook installation. - Add .dockerignore to exclude unnecessary files from the Docker context. - Create extensions.json for VS Code extension recommendations. - Document devcontainer setup and usage in 7_devcontainers.md. - Add run-pip-audit.sh script to manage CVE exclusions from pyproject.toml for consistent auditing. --- .devcontainer/.dockerignore | 18 + .devcontainer/Dockerfile | 15 + .devcontainer/devcontainer.json | 43 +++ .devcontainer/post-create.sh | 27 ++ .github/copilot-instructions.md | 2 +- .github/workflows/ci.yml | 21 ++ .pre-commit-config.yaml | 14 +- .vscode/extensions.json | 11 + .vscode/settings.json | 3 - CONTRIBUTING.md | 57 +-- README.md | 182 +++------- docs/0_setup.md | 516 ++++----------------------- docs/1_usage.md | 4 +- docs/2_secret_management.md | 65 +++- docs/6_pre_commit_troubleshooting.md | 10 +- docs/7_devcontainers.md | 427 ++++++++++++++++++++++ docs/progress.md | 92 ----- docs/quickstart.md | 107 ++++++ pyproject.toml | 5 +- scripts/hooks/black-autoformat.sh | 29 -- scripts/hooks/precommit_pytest.sh | 18 - scripts/{ => hooks}/run-pip-audit.sh | 0 scripts/hooks/verify-lockfile.sh | 48 --- uv.lock | 471 ++++++++++++++++++++++-- 24 files changed, 1340 insertions(+), 845 deletions(-) create mode 100644 .devcontainer/.dockerignore create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/post-create.sh create mode 100644 .vscode/extensions.json create mode 100644 docs/7_devcontainers.md delete mode 100644 docs/progress.md create mode 100644 docs/quickstart.md delete mode 100755 scripts/hooks/black-autoformat.sh delete mode 100755 scripts/hooks/precommit_pytest.sh rename scripts/{ => hooks}/run-pip-audit.sh (100%) delete mode 100755 scripts/hooks/verify-lockfile.sh diff --git a/.devcontainer/.dockerignore b/.devcontainer/.dockerignore new file mode 100644 index 00000000..198488c4 --- /dev/null +++ b/.devcontainer/.dockerignore @@ -0,0 +1,18 @@ +.git +.github +.mypy_cache +.pytest_cache +.ruff_cache +.venv +.coverage +.coverage.* +.direnv +__pycache__ +*.py[cod] +*.egg-info +.ipynb_checkpoints +build +dist +htmlcov +coverage.xml +coverage.svg diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..404bfc23 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/devcontainers/python:1-3.12-bookworm + +ARG USERNAME=vscode +ARG UV_VERSION=0.9.25 + +ADD https://astral.sh/uv/${UV_VERSION}/install.sh /tmp/uv-installer.sh + +RUN env UV_UNMANAGED_INSTALL="/usr/local/bin" sh /tmp/uv-installer.sh \ + && rm /tmp/uv-installer.sh + +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PYTHONDONTWRITEBYTECODE=1 + +RUN mkdir -p /home/${USERNAME}/.cache/uv /home/${USERNAME}/.cache/pre-commit \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.cache diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..b2fa5831 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +{ + "build": { + "context": "..", + "dockerfile": "Dockerfile" + }, + "containerEnv": { + "PRE_COMMIT_HOME": "/home/vscode/.cache/pre-commit", + "UV_CACHE_DIR": "/home/vscode/.cache/uv", + "UV_LINK_MODE": "copy", + "UV_PYTHON_DOWNLOADS": "never" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.mypy-type-checker", + "charliermarsh.ruff", + "ms-toolsai.jupyter" + ], + "settings": { + "jupyter.jupyterServerType": "local", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.terminal.activateEnvironment": true, + "python.testing.pytestArgs": [ + "." + ], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "ruff.enable": true + } + } + }, + "mounts": [ + "source=python-template-uv-cache,target=/home/vscode/.cache/uv,type=volume", + "source=python-template-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume" + ], + "name": "python-basic-template", + "postCreateCommand": "bash .devcontainer/post-create.sh", + "remoteUser": "vscode", + "updateRemoteUserUID": true, + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}" +} diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100644 index 00000000..6e517d41 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${repo_root}" + +echo "Bootstrapping the container workspace with uv..." +uv --version +python --version + +uv sync --frozen + +if [[ -d .git ]]; then + uv run pre-commit install --install-hooks +fi + +if ! uv run python -c "import ipykernel" >/dev/null 2>&1; then + echo "Notebook execution in the container uses the local Python environment and needs ipykernel available there." +fi + +if command -v op >/dev/null 2>&1; then + echo "1Password CLI detected. Run 'op account list' if you need secret-backed examples." +else + echo "1Password CLI is optional and not installed in this container." +fi + +echo "For Windows contributors, keep this repo inside the WSL filesystem before reopening it in the container for the best bind-mount performance." diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c2122b4e..eb2028bb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -244,7 +244,7 @@ ignore = [ ] # Run pip-audit locally to verify -uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc +uv run bash scripts/hooks/run-pip-audit.sh --progress-spinner off --desc # Script automatically reads from pyproject.toml and applies exclusions ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9d9f433..8299b912 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,3 +69,24 @@ jobs: commit_message: "chore: update coverage badge" file_pattern: coverage.svg branch: ${{ github.head_ref }} + + devcontainer-smoke: + name: Devcontainer Smoke + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and bootstrap devcontainer + uses: devcontainers/ci@v0.3 + with: + configFile: .devcontainer/devcontainer.json + push: never + runCmd: | + test -d .venv + test -x .git/hooks/pre-commit + uv --version + .venv/bin/python --version diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f862c1c3..bd71927a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,11 +8,13 @@ repos: language: system types: [jupyter] - # Auto-format Python code with Black - - id: black-autoformat - name: Auto-format Python with Black - entry: uv run scripts/hooks/black-autoformat.sh - language: system + # Python formatter + - repo: https://github.com/psf/black + rev: 25.9.0 + hooks: + - id: black + name: Black (python formatter) + language_version: python3.12 types: [python] - repo: https://github.com/pre-commit/pre-commit-hooks @@ -38,7 +40,7 @@ repos: hooks: - id: pip-audit args: ["--progress-spinner", "off", "--desc"] - entry: uv run bash scripts/run-pip-audit.sh + entry: uv run bash scripts/hooks/run-pip-audit.sh language: system stages: [pre-commit] diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..1d97d6b6 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "ms-vscode-remote.remote-containers", + "ms-azuretools.vscode-docker", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.mypy-type-checker", + "charliermarsh.ruff", + "ms-toolsai.jupyter" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index ee627984..b6c5d03d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,8 +11,5 @@ ], "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, - "ruff.args": [ - "--config=pyproject.toml" - ], "ruff.enable": true } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abfc4f22..eec919c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,9 +5,14 @@ Thank you for your interest in contributing! This repository serves as the official Python project template for TECHLETES, a data & AI consultancy. All contributors are TECHLETES employees, and contributions should reflect our standards of professionalism, quality, and collaboration. -## Prerequisite: 1Password CLI Setup +## Prerequisite: Choose Your Development Environment -Before starting, you must set up the 1Password CLI for secret management. This is required for all TECHLETES Python projects and for running the setup scripts. +Windows contributors should use the devcontainer workflow documented in +`docs/7_devcontainers.md`. Linux and macOS contributors can use the host setup +documented in `docs/0_setup.md`. + +1Password CLI is required only when you need secret-backed workflows on the host. +Inside the devcontainer it is optional. ### 0.1.1 Set-up 1Password (CLI) @@ -42,24 +47,22 @@ Before starting, you must set up the 1Password CLI for secret management. This i ## Getting Started 1. **Clone the repository** (forking is not required for internal TECHLETES projects). -2. **Automatic setup (recommended):** - After cloning, run the setup script to configure your environment: - ```bash - bash scripts/setup.sh - ``` - This script will: - - Check for Python 3.12+, pip, curl, and git - - Create and activate a virtual environment - - Install and lock all dependencies (prod & dev) - - Set up pre-commit hooks - - Check and configure system locale - - Install and configure direnv (if available) - - Initialize secret scanning baseline - - Validate your environment and dependencies - For a detailed breakdown or manual steps, see `docs/0_setup.md`. -3. **Set up pre-commit hooks** (required): +2. **Recommended setup path:** + - On Windows, clone inside WSL storage, open the repo in VS Code, and run + `Dev Containers: Reopen in Container`. + - On Linux/macOS, run the host setup script: + ```bash + ./scripts/setup.sh + ``` + - Do not run `./scripts/setup.sh` inside the devcontainer. The container + bootstraps itself with `uv sync --frozen` and + `uv run pre-commit install --install-hooks`. + For a detailed breakdown, see `docs/0_setup.md` and `docs/7_devcontainers.md`. +3. **Set up pre-commit hooks if needed:** + The devcontainer post-create step already installs them. On host setups, the + setup script installs them as part of onboarding. If you need to reinstall: ```bash - pre-commit install + uv run pre-commit install --install-hooks ``` @@ -98,8 +101,8 @@ This script will: ### 3. Run all checks locally ```bash -pre-commit run --all-files -pytest +uv run pre-commit run --all-files +uv run pytest ``` ### 4. Update documentation if your change affects usage or APIs @@ -137,17 +140,21 @@ pytest For full details, see `docs/0_setup.md`. Key steps include: -1. **System Requirements**: Python 3.12+, pip, curl, git -2. **Virtual Environment**: Remove old `venv`, create and activate new one +1. **System Requirements**: Python 3.12+, git, and either Docker Desktop + WSL2 + for Windows or a host shell environment on Linux/macOS +2. **Virtual Environment**: Use the repo-local `.venv` on both host and + devcontainer workflows 3. **Dependency Management**: Use `uv sync` to install prod/dev dependencies and lock files 4. **Locale Configuration**: Ensure `en_US.UTF-8` is available 5. **direnv Setup**: (Optional) For automatic environment variable loading 6. **Pre-commit Hooks**: Install and update hooks, run all checks -7. **Secret Management**: Set up 1Password CLI and initialize `detect-secrets` baseline +7. **Secret Management**: Set up 1Password CLI only when you need secret-backed + workflows 8. **Tool Verification**: Confirm Black, Ruff, Mypy, Beartype, and pytest are installed 9. **Project Validation**: Import modules, syntax check all Python files, verify dependencies -All of these are automated by `scripts/setup.sh`. +The host path is automated by `scripts/setup.sh`. The container path is automated +by `.devcontainer/post-create.sh`. ## Workplace Standards diff --git a/README.md b/README.md index 535e0f7e..ee58751e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co * Load secrets using 1Password with the helper in code from utils.secrets import get_secret API_KEY = get_secret("op:////") -* See [docs/1_secret_management.md](docs/1_secret_management.md) for mor info on managing secrets. +* See [docs/2_secret_management.md](docs/2_secret_management.md) for more info on managing secrets. ### Dependencies * **Do not** run `pip install` manually in the terminal! @@ -50,76 +50,15 @@ A modern, production-ready Python project template for TECHLETES, a data & AI co ## ๐Ÿš€ Quick Start -### Prerequisites +**For the fastest supported setup path from clone to working environment, see [docs/quickstart.md](docs/quickstart.md).** -- Python 3.12 or later -- git -- curl -- sudo access (for system package installation) -- 1Password CLI (see [docs/0_setup.md](docs/0_setup.md) for setup instructions) -- **uv** (see [Installing uv](#installing-uv) below if you don't have it yet) +The quickstart guide covers: +- Choosing between devcontainer (recommended for Windows, macOS, and mixed-OS teams) and host setup (Linux) +- Step-by-step bootstrap for each path +- First-success verification checks +- First-day commands to run -### Linux: One-Command Setup - -```bash -git clone https://github.com/TECHLETES/{REPO_NAME}.git -cd {REPO_NAME} -./scripts/setup.sh -``` - -### Windows setup - -```powershell -git clone https://github.com/TECHLETES/{REPO_NAME}.git -cd {REPO_NAME} -.\scripts\setup.ps1 -``` - -This will set up everything you need for development! - -### Installing uv - -If you don't have `uv` installed yet, follow these instructions for your platform: - -#### Linux / macOS - -```bash -curl -LsSf https://astral.sh/uv/install.sh | sh -``` - -Then restart your terminal. - -#### Verify Installation - -```bash -uv --version -``` - -You should see something like: `uv 0.x.x` - -### Common setup issues - -#### Issue: Windows setup Powershell Execution Policy - -If you see an error like: - -``` -File .\setup.ps1 cannot be loaded. The file .\setup.ps1 is not digitally signed. You cannot run this script on the current system. For more information about running scripts and setting execution policy, see about_Execution_Policies at https:/go.microsoft.com/fwlink/?LinkID=135170. -``` - -Windows is blocking unsigned scripts by default. To fix this: - -1. Open PowerShell **as your normal user** (no need for admin). -2. Run: - ```powershell - Set-ExecutionPolicy -Scope CurrentUser RemoteSigned -Restart PowerShell and try again: - -```powershell -.\scripts\setup.ps1 -``` - -This only changes the policy for your user account. It allows you to run local scripts while still requiring downloaded scripts to be unblocked (Unblock-File) before execution. +Detailed setup and customization guides are linked in the [๐Ÿ“š Documentation](#-documentation) section. ## ๐Ÿ“‹ Features @@ -141,20 +80,30 @@ This only changes the policy for your user account. It allows you to run local s - **Type checking**: mypy & beartype - **Testing**: pytest with coverage - **Pre-commit hooks** for code quality -- **Jupyter notebook support** with nbstripout +- **Jupyter notebook support** with nbstripout and `ipykernel` ### ๐Ÿ“ฆ **Project Structure** ``` python_basic_template/ +โ”œโ”€โ”€ .devcontainer/ +โ”‚ โ”œโ”€โ”€ Dockerfile # Devcontainer image definition +โ”‚ โ”œโ”€โ”€ devcontainer.json # VS Code devcontainer configuration +โ”‚ โ””โ”€โ”€ post-create.sh # Container bootstrap commands +โ”œโ”€โ”€ .vscode/ +โ”‚ โ””โ”€โ”€ extensions.json # Recommended VS Code extensions +โ”œโ”€โ”€ .dockerignore # Docker build context exclusions โ”œโ”€โ”€ .github/ โ”‚ โ””โ”€โ”€ workflows/ # GitHub Actions workflows -โ”œโ”€โ”€ docs/ # Documentation (setup, secrets, dependencies, quality, progress) +โ”œโ”€โ”€ docs/ # Documentation (setup, secrets, dependencies, quality, containers) +โ”‚ โ”œโ”€โ”€ quickstart.md # โšก Fast path from clone to working environment โ”‚ โ”œโ”€โ”€ 0_setup.md โ”‚ โ”œโ”€โ”€ 1_usage.md -โ”‚ โ”œโ”€โ”€ 1_secret_management.md -โ”‚ โ”œโ”€โ”€ 2_dependency_management.md -โ”‚ โ”œโ”€โ”€ 3_pre_commit_hooks.md +โ”‚ โ”œโ”€โ”€ 2_secret_management.md +โ”‚ โ”œโ”€โ”€ 3_dependency_management.md โ”‚ โ”œโ”€โ”€ 4_code_quality.md +โ”‚ โ”œโ”€โ”€ 5_pre_commit_hooks.md +โ”‚ โ”œโ”€โ”€ 6_pre_commit_troubleshooting.md +โ”‚ โ”œโ”€โ”€ 7_devcontainers.md โ”‚ โ””โ”€โ”€ progress.md โ”œโ”€โ”€ example/ # Example code and notebooks โ”‚ โ”œโ”€โ”€ __init__.py @@ -162,17 +111,16 @@ python_basic_template/ โ”‚ โ””โ”€โ”€ using_secrets.ipynb โ”œโ”€โ”€ scripts/ # Setup and utility scripts โ”‚ โ”œโ”€โ”€ setup.sh -โ”‚ โ”œโ”€โ”€ dependency.sh # uv-based dependency management -โ”‚ โ”œโ”€โ”€ update_deps.py # Update package versions +โ”‚ โ”œโ”€โ”€ run-pip-audit.sh # pip-audit wrapper used in hooks and CI โ”‚ โ”œโ”€โ”€ new-branch.sh โ”‚ โ””โ”€โ”€ hooks/ โ”‚ โ”œโ”€โ”€ black-autoformat.sh โ”‚ โ”œโ”€โ”€ nbstripout-autoadd.sh -โ”‚ โ””โ”€โ”€ precommit_pytest.sh +โ”‚ โ”œโ”€โ”€ precommit_pytest.sh +โ”‚ โ””โ”€โ”€ verify-lockfile.sh โ”œโ”€โ”€ utils/ # Utility modules โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”œโ”€โ”€ secrets.py -โ”‚ โ””โ”€โ”€ utils.py +โ”‚ โ””โ”€โ”€ secrets.py โ”œโ”€โ”€ tests/ # Test directory โ”œโ”€โ”€ pyproject.toml # Project config & dependencies (single source of truth) โ”œโ”€โ”€ uv.lock # Locked dependency versions (auto-generated) @@ -188,52 +136,7 @@ python_basic_template/ This project uses **uv** for fast, reliable dependency management. uv provides a single-file lock format and unified workflow for all dependency tasks. -### First Time Setup - -```bash -# Step 1: Make sure uv is installed (see "Installing uv" section above) -uv --version - -# Step 2: Setup environment and dependencies -./scripts/setup.sh -``` - -The `setup.sh` script will: -1. โœ… Create a virtual environment (`.venv`) -2. โœ… Lock all dependencies into `uv.lock` -3. โœ… Sync and install everything - -**Important:** You don't need to manually activate the virtual environment for most uv commands: -- Use `uv run python script.py` to run Python scripts -- Use `uv run pytest` to run tests -- Use `uv run mypy .` to run type checking - -(Optional) If you prefer traditional shell activation: -```bash -source .venv/bin/activate # Linux/macOS -# or: .venv\Scripts\activate # Windows PowerShell -``` - -### Quick Start - -```bash -# First time: setup environment and lock dependencies -./scripts/setup.sh - -# Day-to-day: sync dependencies (install/update from uv.lock) -uv sync - -# Add a new package -# 1. Edit pyproject.toml and add the package with version constraints -# 2. Run: uv lock && uv sync - -# Update dependencies (updates uv.lock with newer compatible versions) -# For patch + minor updates (safe): -uv lock - -# For major version updates (breaking changes possible): -uv lock --upgrade -``` +After initial setup (see [quickstart.md](docs/quickstart.md)), use these workflows for day-to-day dependency management. ### Dependency Layers @@ -242,8 +145,8 @@ Dependencies are organized into three logical groups: | Layer | Location | Purpose | Installed With | |-------|----------|---------|-----------------| | **Core** | `dependencies` | Essential runtime packages | `uv sync` | -| **Extra** | `[project.optional-dependencies].extra` | Optional production features | `uv sync` | -| **Dev** | `[project.optional-dependencies].dev` | Development and testing tools | `uv sync` (default) | +| **Extra** | `[dependency-groups].extra` | Optional production features | `uv sync --group extra` | +| **Dev** | `[dependency-groups].dev` | Development and testing tools | `uv sync` (default) | **Example structure in `pyproject.toml`:** @@ -255,7 +158,7 @@ dependencies = [ "sqlalchemy", ] -[project.optional-dependencies] +[dependency-groups] # Extra production features extra = [ "redis", @@ -331,18 +234,20 @@ Secure secret handling with 1Password CLI integration: - **detect-secrets** prevents accidental commits - **Example usage** in [example/using_secrets.py](example/using_secrets.py) -**๐Ÿ“– Detailed guide**: [docs/1_secret_management.md](docs/1_secret_management.md) +**๐Ÿ“– Detailed guide**: [docs/2_secret_management.md](docs/2_secret_management.md) ## ๐Ÿ› ๏ธ Development Workflow ### Setup Development Environment ```bash -# Complete setup (recommended for new developers) +# Linux host setup ./scripts/setup.sh ``` -**For manual set-up see**: [docs/0_setup.md](docs/0_setup.md) +Use [docs/quickstart.md](docs/quickstart.md) to choose between the devcontainer path and the Linux host setup path. + +**For detailed setup guidance see**: [docs/0_setup.md](docs/0_setup.md) ### Code Quality @@ -408,7 +313,7 @@ All development tools are configured in `pyproject.toml`: 2. **Update project metadata** in `pyproject.toml`: - Change `name`, `description`, `authors` - Update repository URLs -3. **Run setup**: `./scripts/setup.sh` +3. **Bootstrap your environment** with [docs/quickstart.md](docs/quickstart.md) 4. **Start coding**! ### Customization @@ -420,12 +325,17 @@ All development tools are configured in `pyproject.toml`: ## ๐Ÿ“š Documentation -0. **[Setup](docs/0_setup.md)** - Guide to setup the development environment +**Start here:** +- **[Quickstart](docs/quickstart.md)** - Fastest path from clone to working environment + +**Detailed guides:** +0. **[Setup](docs/0_setup.md)** - Full guide to setup the development environment 1. **[Usage](docs/1_usage.md)** - Guide for daily usage of this repository 2. **[Secret Management](docs/2_secret_management.md)** - Secure secret handling guide 3. **[Dependency Management](docs/3_dependency_management.md)** - Detailed dependency workflow 4. **[Code Quality](docs/4_code_quality.md)** - Rules and guidelines on code quality and how it is enforced 5. **[Pre Commit Hooks](docs/5_pre_commit_hooks.md)** - Pre commit hooks to ensure safety and quality +6. **[Devcontainers](docs/7_devcontainers.md)** - Devcontainer setup, customization, and troubleshooting ## ๐Ÿค Contributing @@ -442,9 +352,7 @@ This project is licensed under the MIT License - see the LICENSE file for detail **Virtual environment issues:** ```bash -rm -rf venv -python3 -m venv venv -source venv/bin/activate +rm -rf .venv uv sync ``` @@ -462,7 +370,7 @@ pre-commit run --all-files **Dependency conflicts:** ```bash pip check # Identify conflicts -# Review and resolve in requirements.in or pyproject.toml +# Review and resolve in pyproject.toml, then run uv lock && uv sync ``` --- diff --git a/docs/0_setup.md b/docs/0_setup.md index db0d7422..0e3bbf76 100644 --- a/docs/0_setup.md +++ b/docs/0_setup.md @@ -1,46 +1,45 @@ - ### ๐Ÿ“„ `0_setup.md` - Project Setup Guide -This guide will walk you through setting up the `python_template` development environment on your local machine. - -## ๐Ÿš€ Quick Start: One-Command Setup +> **๐Ÿ‘‰ First time?** See [docs/quickstart.md](quickstart.md) for the fastest supported path from clone to working environment. This guide is the detailed reference for setup paths, prerequisites, and troubleshooting. -These scripts automate the entire setup process, including dependency installation, virtual environment creation, and tool configuration. +This guide helps you choose the right onboarding path and troubleshoot setup issues. This repository is a template intended to be copied into new projects, so pick whichever approach works best for your team. -### Prerequisites +Today, the documented host automation is Linux-first. Windows and macOS contributors should prefer the devcontainer workflow unless they are intentionally maintaining their own local setup outside this script. -Before you begin, ensure you have the following installed: +## ๐Ÿš€ Setup Path Selection -* **Python 3.12 or later** -* **Git** -* **1Password CLI**: Required for secret management. Follow the [official installation guide](https://developer.1password.com/docs/cli/get-started/). -* **cURL**: Usually pre-installed on Linux/macOS. -* **Sudo/Administrator access**: For installing system packages like `direnv`. +Choose the workflow that matches your platform and preferences. --- -### Linux / macOS Setup +## Windows: Devcontainer (Recommended) -Open your terminal and run the following commands: +Windows contributors should use the devcontainer workflow for the most consistent experience. -```bash -# 1. Clone the repository -git clone https://github.com/TECHLETES/{REPO_NAME}.git +**Prerequisites:** +- Docker Desktop +- WSL2 enabled +- VS Code with the Dev Containers extension -# 2. Navigate into the project directory -cd {REPO_NAME} +**Steps:** +1. Clone the repository inside the WSL filesystem (e.g., `~/src`). +2. Open the folder in VS Code from WSL. +3. Run the `Dev Containers: Reopen in Container` command. +4. Wait for the post-create bootstrap to finish. -# 3. Run the setup script -./scripts/setup.sh -``` +**Do not** run `./scripts/setup.sh` inside the devcontainerโ€”the container bootstrap handles everything. + +For detailed information about how the devcontainer works, customization options, and platform-specific setup, see [docs/7_devcontainers.md](7_devcontainers.md). --- -### Windows Setup +## Linux: Host Setup -Open PowerShell and run the following commands: +Use this path when you want to work directly on a Linux host. If you are on macOS, use the devcontainer path above instead of relying on `./scripts/setup.sh`. -```powershell +Open your terminal and run: + +```bash # 1. Clone the repository git clone https://github.com/TECHLETES/{REPO_NAME}.git @@ -48,466 +47,87 @@ git clone https://github.com/TECHLETES/{REPO_NAME}.git cd {REPO_NAME} # 3. Run the setup script -# If activation is blocked: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned -.\scripts\setup.ps1 +./scripts/setup.sh ``` -After the script finishes, **restart your terminal or PowerShell session** to ensure all environment changes are applied correctly. +The setup script handles the current Linux host bootstrap, including Python +environment setup, dependency installation with `uv`, pre-commit hook setup, +and bash-based direnv integration. -## ๐Ÿค” What the Setup Script Does +--- -The setup script is designed to create a consistent and reproducible development environment by automating the following steps: +## ๐Ÿ›Ÿ Quick Troubleshooting -1. **System Checks**: Verifies that Python, Git, and other required tools are installed. -2. **Virtual Environment**: Creates an isolated Python virtual environment in a `.venv/` directory. -3. **Dependency Installation**: Installs all production and development dependencies using `uv` for fast, reproducible builds. -4. **Lock File Generation**: Creates `uv.lock` to ensure consistent dependency versions across all team members and CI/CD. -5. **Pre-Commit Hooks**: Sets up automated code quality checks that run before every commit. -6. **Secret Management**: Initializes `detect-secrets` to prevent accidental credential exposure and prepares for 1Password integration. -7. **Environment Variables**: Configures `direnv` (on Linux/macOS) to automatically manage environment variables when you enter the project directory. +
+Windows: Docker or VS Code setup questions -## ๐Ÿ›Ÿ Common Setup Issues +See [docs/7_devcontainers.md](7_devcontainers.md) for complete devcontainer +setup guidance, including Docker Desktop configuration and WSL filesystem recommendations. + +
-Windows: PowerShell script execution is blocked +Linux host setup fails or has warnings -If you see an error like `File .\setup.ps1 cannot be loaded... not digitally signed`, your system is blocking unsigned PowerShell scripts. +The setup script auto-detects your package manager (apt-get, dnf, yum) and +installs missing dependencies. If it fails: -**Solution:** -1. Open a new PowerShell window **as your normal user** (not as Administrator). -2. Run the following command to allow local scripts to run: - ```powershell - Set-ExecutionPolicy -Scope CurrentUser RemoteSigned - ``` -3. Close the PowerShell window and re-run the setup script in a new one. +1. Check that you have `sudo` access. +2. Ensure Git and Python 3.12+ are installed. +3. Re-run `./scripts/setup.sh`โ€”it is safe to run multiple times.
-Linux: `python3.12-venv` not found +Linux host virtual environment corrupted -On some Linux distributions (like Debian/Ubuntu), the `venv` module is a separate package. The setup script will attempt to install it automatically. +Delete the environment and let the script recreate it: -**Solution:** -If the automatic installation fails, you can install it manually: ```bash -sudo apt-get update -sudo apt-get install python3.12-venv +rm -rf .venv +./scripts/setup.sh ``` -Then, re-run `./scripts/setup.sh`. -
- -
-1Password CLI: Not signed in or not linked to WSL -The setup script may warn that the `op` command-line tool is not signed in. - -**Solution:** -1. **Sign In**: Run `op account list`. If you see "No accounts configured," run `op signin` and follow the prompts. -2. **WSL Users**: If you use the 1Password desktop app on Windows, you must enable WSL integration in the app's settings: `Settings > Developer > Integrate with Windows Subsystem for Linux`. +Or manually refresh with `uv sync`.
-Virtual Environment is Corrupted +1Password CLI not available -If you encounter strange dependency or pathing issues, your virtual environment might be in a bad state. +The `op` command-line tool is optional for getting started. If you need secrets: -**Solution:** -1. Delete the existing virtual environment directory. - ```bash - # On Linux/macOS - rm -rf venv +- See [docs/2_secret_management.md](2_secret_management.md) for setup steps. +- Or contact your team lead for your 1Password integration approach. - # On Windows - Remove-Item -Recurse -Force venv - ``` -2. Re-run the setup script (`./scripts/setup.sh` or `.\scripts\setup.ps1`) to create a fresh one.
-*** - -## ๐Ÿงฐ Manual setup (for advanced users) - -If you prefer manual control, follow these steps in order. - -## **1 Prerequisites** - -Ensure the following tools are installed: - -- Python 3.12 or later -- Git -- curl -- 1Password CLI -- Sudo/admin access (for direnv, etc.) - -### 1.1 Set-up 1Password (CLI) - -1. **Download and activate 1Password Desktop** - 1. Download and install from here: - - [Download 1Password for Windows | 1Password](https://1password.com/downloads/windows) - - 2. Log in with your account. - 3. Go to โ€˜Settingsโ€™ > โ€˜Securityโ€™ > Enable โ€˜Unlock with Windows Helloโ€™ - 4. Go to โ€˜settingsโ€™ > โ€˜Developerโ€™ > Enable โ€˜Integrate with 1Password CLIโ€™. - -2. **Set-up 1password cli** - 1. Open windows powershell and run - - ```bash - winget install 1password-cli - ``` - - 2. In your file explorer navigate to: - - `C:/Users//AppData/Local/Microsoft/WinGet/Packages/<1password_cli_package_id>/op.exe` and copy the absolute path that leads to the `op.exe` - - 3. You then modify the path by replacing `C:`/ with `/mnt/c/` . Also replace all backward slashes with forward slashes. Your final path should look something like: - - `/mnt/c/Users/thome/AppData/Local/Microsoft/WinGet/Packages/AgileBits.1Password.CLI_Microsoft.Winget.Source_8wekyb3d8bbwe/op.exe` - - 4. Then in WSL run the following command: - - ```bash - ln -s /usr/bin/op - ``` - - 5. Run in WSL: - - ```bash - op --version - ``` - - If you see a version number, then the link was succesfull and you can access 1Password in WSL +--- +## โ„น๏ธ What the Setup Script Does ---- +The `./scripts/setup.sh` script automates: -## 2 Automatic set-up script +1. **System requirements check** โ€“ Verifies Python 3.12+, Git, `uv`, `direnv`, and other required tools, installing missing packages with `apt-get`, `dnf`, or `yum` when possible +2. **Virtual environment and dependencies** โ€“ Materializes `.venv/`, runs `uv lock`, and runs `uv sync` +3. **Pre-commit hooks** โ€“ Installs Git hooks, runs `pre-commit autoupdate`, and runs `pre-commit run --all-files` +4. **Secrets baseline** โ€“ Creates `.secrets.baseline` with `detect-secrets` +5. **direnv configuration** โ€“ Appends the bash hook to `~/.bashrc` and runs `direnv allow` when `.envrc` exists +6. **System configuration** โ€“ Configures locales and, on `apt-get`-based Linux, installs Docker and Docker Compose if they are missing -To simplify the setup process, all mandatory setup steps have been implemented into a single, automated script. After cloning or forking this repository, you can configure your entire development environment by simply running: +After the script completes, your environment is ready to use. Activate the +virtual environment in new terminal sessions: ```bash -bash scripts/setup.sh +source .venv/bin/activate ``` -This script will take care of everything needed to get started, so you can focus on writing code right away. - -If you'd prefer to walk through the setup manually or want to understand what's happening under the hood, a detailed breakdown of each step is provided in the sections below. - -## 3 Manual set-up development environment - -Follow these steps to prepare your local machine for development with the Python Basic Template. Perform each section in order. - ---- - -### 3.1 System Requirements Check - -1. **Verify Python 3** - - ```bash - python3 --version - ``` - - - Ensure the version is **3.12** or later. -2. **Verify pip** - - ```bash - pip --version - ``` - -3. **Verify curl** - - ```bash - command -v curl - ``` - -4. **Verify git** - - ```bash - git --version - ``` - - -If any of these commands fail, install the missing package before proceeding. - ---- - -### 3.2 Virtual Environment Setup - -1. **Remove existing `venv`** (if present) - - ```bash - rm -rf venv - ``` - -2. **Create a new virtual environment** - - ```bash - python3 -m venv venv - ``` - -3. **Activate the virtual environment** - - ```bash - source venv/bin/activate - ``` - -4. **Upgrade pip** - - ```bash - pip install --upgrade pip - ``` - - ---- - -### 3.3 Hybrid Dependency Management - -This project uses a hybrid approach to dependency management: - -- **Production dependencies**: declared in `pyproject.toml`, locked to `requirements.txt`. -- **Development dependencies**: listed under `project.optional-dependencies.dev` in `pyproject.toml`, locked to `dev-requirements.txt`. - -You can use the helper script or perform each step manually: - -**3.3.1 Using `uv sync`** - -- Install both production and development dependencies: - - ``` - uv sync - ``` - -- Install only production dependencies: - - ``` - uv sync --no-dev - ``` - -- Install only development dependencies: - - ``` - uv sync --only-dev - ``` - - -**3.3.2 Manual Steps** - -If you prefer to run each step manually, follow these commands: - -1. **Activate the virtual environment** (must be in project root): - - ``` - source venv/bin/activate - ``` - -2. **Ensure `pip-tools` is installed**: - - ``` - pip show pip-tools &>/dev/null || pip install pip-tools - ``` - -3. **Compile production lockfile**: - - ``` - pip-compile pyproject.toml \ - --output-file=requirements.txt \ - --generate-hashes - ``` - -4. **Install production dependencies**: - - ``` - pip install --require-hashes -r requirements.txt - ``` - -5. **Compile development lockfile**: - - ``` - pip-compile pyproject.toml \ - --extra=dev \ - --output-file=requirements-dev.txt \ - --generate-hashes - ``` - -6. **Install development dependencies**: - - ``` - pip install --require-hashes -r requirements-dev.txt - ``` - -7. **Verify that all packages are compatible**: - - ``` - pip check - ``` - - ---- - -### 3.4 System Locale Configuration - -1. **Update package lists and install locales** - - ```bash - sudo apt update - sudo apt install -y locales - ``` - -2. **Generate and apply the `en_US.UTF-8` locale** - - ```bash - sudo locale-gen en_US.UTF-8 - sudo update-locale LANG=en_US.UTF-8 - ``` - - ---- - -### 3.5 direnv Setup - -1. **Install `direnv`** - - ```bash - sudo apt install -y direnv - ``` - -2. **Add the `direnv` hook to your shell profile** - - ```bash - echo 'eval "$(direnv hook bash)"' >> ~/.bashrc - source ~/.bashrc - ``` - -3. **Allow the project `.envrc`** - - ```bash - cd path/to/project - direnv allow - ``` - - ---- - -### 3.6 Pre-commit Hooks Setup - -1. **Install Git hooks** - - ```bash - pre-commit install - ``` - -2. **Update hook definitions** - - ```bash - pre-commit autoupdate - - ``` - -3. **Run all checks once** - - ```bash - pre-commit run --all-files - - ``` - - ---- - -### 3.7. Secret Management Setup - -1. **Check for 1Password CLI** - - ```bash - op --version - ``` - -2. **Verify your 1Password account** - - ```bash - op account list - ``` - -3. **Initialize a baseline for `detect-secrets`** - - ```bash - mv .secrets.baseline .secrets.baseline.bak 2>/dev/null || true - detect-secrets scan > .secrets.baseline - detect-secrets audit .secrets.baseline - ``` - - ---- - -### 3.8 Development Tools Verification - -1. **Black** - - ```bash - pip show black - ``` - -2. **Ruff** - - ```bash - pip show ruff - ``` - -3. **Mypy** - - ```bash - pip show mypy - ``` - -4. **Beartype** - - ```bash - pip show beartype - ``` - -5. **pytest** - - ```bash - pip show pytest - - ``` - - ---- - -### 3.9 Project Validation - -1. **Ensure all modules import correctly** - - ```bash - python3 - << 'EOF' - import utils.secrets - import utils.utils - print("Imports successful") - EOF - - ``` - -2. **Syntax check all Python files** - - ```bash - find . -name "*.py" -not -path "./venv/*" \ - -exec python3 -m py_compile {} + - ``` - -3. **Verify installed dependencies** - - ```bash - pip check - ``` - +Or use `direnv allow` to auto-activate when you enter the project directory. If you use a shell other than bash, add the appropriate `direnv` hook manually. --- -### 3.10 Next Steps +## Next Steps -- Restart your terminal or run `source ~/.bashrc` -- Start development: run your application or tests locally -- Refer to the project README for usage and contribution guidelines +- See [docs/1_usage.md](1_usage.md) for daily development workflows +- See [docs/2_secret_management.md](2_secret_management.md) for secret handling +- See [docs/7_devcontainers.md](7_devcontainers.md) for devcontainer details and customization diff --git a/docs/1_usage.md b/docs/1_usage.md index 40f0a57a..d0d8872b 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -69,7 +69,7 @@ If `pip-audit` reports a vulnerability that you need to exclude (false positive, **1. Run pip-audit to see the vulnerability:** ```bash -uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc +uv run bash scripts/hooks/run-pip-audit.sh --progress-spinner off --desc ``` **2. Add the CVE to `pyproject.toml`:** @@ -95,7 +95,7 @@ ignore = [ pre-commit run pip-audit --all-files # Or run directly -uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc +uv run bash scripts/hooks/run-pip-audit.sh --progress-spinner off --desc ``` **Important:** diff --git a/docs/2_secret_management.md b/docs/2_secret_management.md index 853ff5f4..dc7f8c0b 100644 --- a/docs/2_secret_management.md +++ b/docs/2_secret_management.md @@ -54,7 +54,56 @@ On each commit, secrets are blocked and notebook outputs cleared: --- -## 1.2 CI / GitHub Actions +## 1.2 Devcontainer Development + +### Secret-Specific Behavior + +The devcontainer itself **boots with or without** the 1Password CLI (`op`): + +- **If `op` is available** in the container, use `get_secret()` normally: + ```python + from utils.secrets import get_secret + + api_key = get_secret("op://Vault/Item/field") + ``` + +- **If `op` is unavailable or not authenticated**, `get_secret()` will fail because it shells out to `op read ...`. For development, guard secret-backed code with an explicit environment-variable path instead of assuming the devcontainer provides a readiness flag: + ```python + import os + import shutil + + from utils.secrets import get_secret + + # Fall back to an environment variable when 1Password is unavailable. + if shutil.which("op") and os.getenv("OP_SERVICE_ACCOUNT_TOKEN"): + api_key = get_secret("op://Vault/Item/field") + else: + api_key = os.getenv("API_KEY_DEV") + ``` + +The post-create script only reports whether `op` is installed. It does not +install 1Password CLI, authenticate it, or export an `OP_AVAILABLE` variable. + +### Setup, Customization & Troubleshooting + +For general devcontainer setup, prerequisites, Windows/WSL guidance, and troubleshooting, see **[docs/7_devcontainers.md](./7_devcontainers.md)**. + +### Tests with Secrets + +Unit tests that require secrets should mock `get_secret()` or use environment variables: + +```python +from unittest.mock import patch + +def test_with_mock_secret() -> None: + with patch("utils.secrets.get_secret", return_value="mock-secret"): + result = my_function_that_needs_secret() + assert result is not None +``` + +--- + +## 1.3 CI / GitHub Actions For CI, we **do not** use 1Password directly. Instead, define your secrets in **GitHub Settings > Secrets**: @@ -75,18 +124,24 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.12' + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + - name: Install dependencies - run: pip install -r requirements.txt + run: uv sync - name: Run tests env: API_KEY: ${{ secrets.API_KEY }} DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - run: pytest + run: uv run pytest ``` @@ -94,7 +149,7 @@ This way, CI runners receive secrets securely from GitHub and never see 1Passwor --- -## 1.3 Rotating & Auditing +## 1.4 Rotating & Auditing - **Rotate** immediately in 1Password or GitHub when a secret is compromised. Follow this action plan: diff --git a/docs/6_pre_commit_troubleshooting.md b/docs/6_pre_commit_troubleshooting.md index 1825732a..45054cae 100644 --- a/docs/6_pre_commit_troubleshooting.md +++ b/docs/6_pre_commit_troubleshooting.md @@ -400,7 +400,7 @@ python -m json.tool .secrets.baseline > /dev/null ```toml [tool.pip-audit] # Centralized CVE/PYSEC exclusions for pip-audit -# Used by both pre-commit hooks and CI workflows via scripts/run-pip-audit.sh +# Used by both pre-commit hooks and CI workflows via scripts/hooks/run-pip-audit.sh ignore = [ "CVE-2026-1703", # Path traversal in pip (only affects pip extraction) "CVE-2024-XXXXX", # Your new exclusion with explanation @@ -410,7 +410,7 @@ ignore = [ **Workflow:** 1. Run pip-audit to see the vulnerability: ```bash - uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc + uv run bash scripts/hooks/run-pip-audit.sh --progress-spinner off --desc ``` 2. Identify the CVE/PYSEC ID from the output 3. Add the ID to the `ignore` list in `pyproject.toml` @@ -419,7 +419,7 @@ ignore = [ **Important:** - All exclusions go in **ONE place**: `pyproject.toml [tool.pip-audit]` -- The `scripts/run-pip-audit.sh` wrapper reads from `pyproject.toml` and applies the exclusions +- The `scripts/hooks/run-pip-audit.sh` wrapper reads from `pyproject.toml` and applies the exclusions - Never add `--ignore-vuln` directly to pre-commit config or CI workflowโ€”it goes out of sync - Commit changes to `pyproject.toml` like any other code change @@ -437,7 +437,7 @@ uv lock --upgrade-package uv sync # Run pip-audit to verify -uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc +uv run bash scripts/hooks/run-pip-audit.sh --progress-spinner off --desc ``` #### Verifying Exclusions Work @@ -445,7 +445,7 @@ uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc **Test locally:** ```bash # Run pip-audit with exclusions from pyproject.toml -uv run bash scripts/run-pip-audit.sh --progress-spinner off --desc +uv run bash scripts/hooks/run-pip-audit.sh --progress-spinner off --desc # Or run pre-commit manually pre-commit run pip-audit --all-files diff --git a/docs/7_devcontainers.md b/docs/7_devcontainers.md new file mode 100644 index 00000000..6b34dfee --- /dev/null +++ b/docs/7_devcontainers.md @@ -0,0 +1,427 @@ +## Devcontainer Guide + +This document is the canonical reference for the devcontainer setup that ships +with this template. Teams copying this repository into a new project should use +this guide to understand what the container does today, why it exists, and +which files to change when they need project-specific behavior. + +## What Devcontainers Are + +A devcontainer is a development environment defined as code. In VS Code, the +Dev Containers extension reads `.devcontainer/devcontainer.json`, builds or +pulls the configured container image, mounts the repository into that +container, and then opens the workspace inside it. + +In practice, that means: + +1. VS Code reads the devcontainer configuration. +2. Docker builds the image defined by `.devcontainer/Dockerfile`. +3. VS Code starts a container from that image. +4. Your repository is mounted into the container workspace. +5. VS Code runs any lifecycle command such as `postCreateCommand`. +6. VS Code applies container-specific settings and extension installs. + +The result is that contributors work against the same OS base image, the same +Python version, the same toolchain, and the same workspace bootstrap sequence, +regardless of what is installed on the host machine. + +## Why This Template Includes Devcontainers + +This template is meant to be copied into new projects, including teams with a +mix of Windows, macOS, and Linux laptops. The devcontainer exists to reduce the +amount of host-specific setup work that every derived repository has to explain +and debug. + +The main reasons this template includes a devcontainer are: + +- Consistency: contributors get the same Python base image and the same + bootstrapped workspace. +- Faster onboarding: a new developer can open the repo in VS Code and let the + container provision the environment. +- Better Windows support: the container avoids many host Python and shell setup + differences when paired with WSL2. +- Closer parity between development and automation: this repository's CI + includes a devcontainer smoke job that builds the container and verifies that + the workspace bootstraps successfully. + +Devcontainers are not mandatory for every contributor. Linux and macOS users can +still use the host setup from `docs/0_setup.md` if they prefer. The devcontainer +is the template's supported containerized path, not the only path. + +## How This Template's Devcontainer Works + +The current implementation is intentionally small. It gives teams a working +baseline without hiding much behavior. + +### Image and Tooling + +The container image is built from `.devcontainer/Dockerfile`, which currently: + +- Starts from `mcr.microsoft.com/devcontainers/python:1-3.12-bookworm`. +- Installs a pinned version of `uv` into `/usr/local/bin`. +- Pre-creates cache directories for `uv` and `pre-commit` under the `vscode` + user's home directory. +- Sets a couple of Python-related environment defaults such as disabling pip's + version check and avoiding `.pyc` generation. + +This is a good template default because the image stays easy to reason about. +Python and `uv` are baked into the image, while project dependencies are still +installed from the repository lock file during workspace bootstrap. + +### Container Configuration + +The runtime behavior lives in `.devcontainer/devcontainer.json`. In this +template, that file currently does the following: + +- Builds from the local `Dockerfile` in the `.devcontainer` directory. +- Opens the repository at `/workspaces/${localWorkspaceFolderBasename}`. +- Uses `vscode` as the remote user and updates the container UID to match the + host user when possible. +- Defines cache locations for `pre-commit` and `uv`. +- Mounts named Docker volumes for those caches so repeated container rebuilds do + not have to start from an empty cache. +- Runs `bash .devcontainer/post-create.sh` after the container is created. +- Installs container-specific VS Code extensions for Python, Pylance, mypy, + Ruff, and Jupyter. +- Sets container-specific VS Code settings such as the default interpreter and + pytest configuration. + +Two details matter for derived repositories: + +- The named cache volumes currently use template-specific names. When a team + copies this template into a new project, those names should usually be + renamed so cache volumes are clearly associated with the new repository. +- The interpreter path is set to `${workspaceFolder}/.venv/bin/python`, so the + container is expected to create a repository-local virtual environment rather + than using a global interpreter. + +### Workspace Bootstrap + +The first-time workspace setup happens in `.devcontainer/post-create.sh`. That +script currently: + +1. Changes to the repository root. +2. Prints the `uv` and Python versions for visibility. +3. Runs `uv sync --frozen`. +4. Installs pre-commit hooks if the repository has a `.git` directory. +5. Checks whether `ipykernel` is importable and prints a note if it is not. +6. Prints a message about the `op` CLI being optional. +7. Prints the Windows/WSL recommendation to keep the repo in the WSL filesystem. + +This means the container assumes the repository already contains a valid +`pyproject.toml` and `uv.lock`, and that `uv sync --frozen` is the correct way +to materialize the working environment. + +### VS Code Experience + +The template configures both container-specific editor behavior and workspace +recommendations: + +- `.devcontainer/devcontainer.json` installs extensions automatically when the + repo is opened in the container. +- `.vscode/extensions.json` recommends the same core Python and container tools + for people opening the repository outside the container. + +That split is deliberate. The devcontainer file controls the container session. +The workspace recommendations help contributors before they ever reopen the repo +in the container, and they still help contributors who stay on the host. + +### CI Coverage + +This repository also checks the devcontainer in CI. The `devcontainer-smoke` job +in `.github/workflows/ci.yml` builds the devcontainer and verifies that: + +- A repo-local `.venv` exists. +- The Git pre-commit hook was installed. +- `uv` is available. +- The project Python interpreter exists inside `.venv`. + +That is useful context for teams customizing a derived repository: if you change +the devcontainer behavior, make sure the container still bootstraps a usable +workspace rather than only building successfully. + +## Platform Recommendations + +### Windows and WSL2 + +For Windows contributors, the recommended workflow is: + +1. Install Docker Desktop. +2. Enable WSL2. +3. Enable Docker Desktop integration with your WSL distro. +4. Install VS Code and the Dev Containers extension. +5. Clone the repository inside the WSL filesystem, not under a Windows-mounted + path such as `/mnt/c/...`. +6. Open the folder in VS Code from WSL. +7. Run `Dev Containers: Reopen in Container`. + +The WSL filesystem recommendation matters because bind-mounted source trees are +usually much slower when the repository lives on the Windows filesystem. This +template's post-create script prints that recommendation as a reminder. + +### Linux and macOS + +Linux and macOS contributors can choose either workflow: + +- Use the host setup described elsewhere in the docs. +- Use the devcontainer for consistency with other contributors. + +If you use the devcontainer on Linux or macOS, the main thing to know is that +your source code is still the local repository, bind-mounted into the +containerized workspace. Rebuilding the container does not delete your Git +working tree, but it can require the environment bootstrap to run again. + +## Day-to-Day Workflow Inside the Container + +Once the container is up, day-to-day work should feel close to working on the +host, with a few important differences. + +### Opening the Project + +On the first open or after a rebuild: + +1. Reopen the repository in the container. +2. Wait for the post-create script to finish. +3. Confirm that VS Code is using `${workspaceFolder}/.venv/bin/python`. +4. Start working normally from the integrated terminal. + +Do not run `./scripts/setup.sh` inside the container. This template already uses +the image build plus `.devcontainer/post-create.sh` as the supported bootstrap +path for containerized development. + +### Normal Development Commands + +Typical commands inside the container are the same ones you would run on the +host: + +```bash +uv run pytest +uv run mypy . --config-file=pyproject.toml +uv run ruff check . +uv run pre-commit run --all-files +``` + +If you change dependencies, keep following the repository's normal dependency +workflow: + +1. Edit `pyproject.toml`. +2. Regenerate the lock file. +3. Run `uv sync` again. + +The devcontainer does not replace the repository's dependency management model. +It only provides the environment where that model runs. + +### Notebooks and Jupyter + +The devcontainer installs the Jupyter extension in VS Code and expects notebook +execution to use the local project environment in `.venv`. The post-create +script checks for `ipykernel` and prints a warning if it is missing. If notebook +support looks broken, the first thing to try is re-running `uv sync --frozen`. + +### Secrets and 1Password + +The container does not assume that the `op` CLI is installed or authenticated. +The post-create script only reports whether it is present. Teams should not rely +on automatic 1Password integration just because the repo is running inside a +devcontainer. + +## How To Customize This Setup In A Derived Repository + +When a team copies this template to start a new project, the right question is +usually not "should we keep the devcontainer?" but "which layer should we +change?" Use the smallest layer that matches the change you need. + +### Recommended Customization Approach + +Use this order of operations: + +1. Keep the base structure unless the new project has a real need to diverge. +2. Rename template-specific labels such as the devcontainer name and cache + volume names. +3. Add project-specific OS packages or CLIs in the Dockerfile. +4. Add project-specific bootstrap steps in `post-create.sh`. +5. Adjust VS Code settings and extension recommendations only when the project + genuinely needs them. + +Avoid pushing every change into `post-create.sh`. If a dependency is required in +every container instance, it usually belongs in the image build rather than in a +per-workspace bootstrap script. + +### File Map: What To Edit And When + +#### `.devcontainer/devcontainer.json` + +Edit this file when you need to change how VS Code creates and launches the +development container. + +Typical reasons to edit it: + +- Rename the devcontainer for the new project. +- Change environment variables used by tools in the container. +- Change mounts, cache volume names, or workspace behavior. +- Change the lifecycle command that runs after container creation. +- Add or remove VS Code extensions that should be installed automatically inside + the container. +- Add or adjust container-specific VS Code settings. + +Do not use this file for large package installation scripts when those packages +can be installed once in the image instead. + +#### `.devcontainer/Dockerfile` + +Edit this file when you need to change the image itself. + +Typical reasons to edit it: + +- Add system packages required by your project. +- Install a language runtime, CLI, or build tool that should exist in every + container instance. +- Change the base image or pinned tool versions. +- Pre-create directories or other image-level filesystem state. + +If a tool is required before `post-create.sh` can run, it belongs here. + +#### `.devcontainer/post-create.sh` + +Edit this file when you need to change what happens after the repository is +mounted and the container is first created. + +Typical reasons to edit it: + +- Bootstrap project dependencies from lock files. +- Install Git hooks. +- Run one-time project setup that depends on the checked-out repository. +- Print guidance that contributors should see on first open. + +Keep this script idempotent when possible. Contributors may rerun it manually, +and container rebuilds should not depend on fragile one-off state. + +#### `.vscode/extensions.json` + +Edit this file when you need to change which extensions VS Code recommends for +the repository in general, including host-based workflows. + +Typical reasons to edit it: + +- Recommend Docker or Dev Containers tooling. +- Recommend language or linting extensions that help contributors even before + they reopen the repo in the container. +- Keep workspace recommendations aligned with the languages and tools used by + the project. + +Use this file for recommendations. Use `.devcontainer/devcontainer.json` for +extensions that should be installed automatically in the container session. + +## Troubleshooting And Common Failure Modes + +### The Container Does Not Start At All + +Common causes: + +- Docker Desktop or the local Docker engine is not running. +- On Windows, WSL2 integration is not enabled. +- The user opened the repo in a context where the Dev Containers extension + cannot talk to Docker. + +Start by confirming that Docker works on the host, then try `Dev Containers: +Rebuild and Reopen in Container`. + +### The Container Builds, But The Workspace Is Not Ready + +In this template, the most important workspace bootstrap step is: + +```bash +uv sync --frozen +``` + +If that step fails, the container may open without a working `.venv` or without +project dependencies installed. + +Typical causes: + +- `uv.lock` is missing, stale, or inconsistent with `pyproject.toml`. +- Network or registry access is blocked while fetching dependencies. +- A derived repository changed bootstrap expectations without updating the + devcontainer scripts. + +First response: + +```bash +uv sync --frozen +uv run pre-commit install --install-hooks +``` + +If the first command fails, fix the dependency or lock-file issue rather than +retrying the container repeatedly. + +### VS Code Uses The Wrong Python Interpreter + +The intended interpreter is `${workspaceFolder}/.venv/bin/python`. If VS Code is +not using it: + +1. Check whether `.venv` exists. +2. Re-run `uv sync --frozen` if it does not. +3. Use the VS Code Python interpreter selector to point back to the repo-local + virtual environment. +4. Rebuild the container if the environment was created in a broken state. + +### Pre-commit Hooks Were Not Installed + +The post-create script only installs hooks when a `.git` directory exists. That +means hook installation can be skipped if someone opens a source snapshot +without Git metadata. + +Manual fix: + +```bash +uv run pre-commit install --install-hooks +``` + +### Jupyter Cannot Find A Kernel + +The template expects notebook execution to use packages installed into the local +project environment. Re-run: + +```bash +uv sync --frozen +``` + +If the problem persists in a derived repository, verify that `ipykernel` is +still part of the environment expected for notebook work. + +### `op` Is Missing In The Container + +That is expected in this template. The post-create script explicitly treats the +1Password CLI as optional. If a derived repository requires it, that team must +decide whether to install it in the image, document a manual setup step, or use +another secret-access pattern. + +### A Team Changed The Devcontainer And CI Started Failing + +Remember that this template's CI does not just build the image. It checks that a +usable workspace exists after bootstrap. If the devcontainer smoke job fails, +look for regressions such as: + +- `.venv` is no longer created. +- The pre-commit hook is no longer installed. +- `uv` is missing from the image. +- The expected Python interpreter path changed. + +When you customize a derived repository, keep the bootstrap contract explicit +and update CI checks if the contract intentionally changes. + +## Practical Rules Of Thumb + +- If the change is about Docker image contents, edit `.devcontainer/Dockerfile`. +- If the change is about container launch behavior, edit + `.devcontainer/devcontainer.json`. +- If the change is about project bootstrap after checkout, edit + `.devcontainer/post-create.sh`. +- If the change is about editor recommendations for all contributors, edit + `.vscode/extensions.json`. +- If you changed container build or launch behavior, rebuild the container. +- If you changed only repository bootstrap steps, rerunning the post-create + script may be enough, but a rebuild is the safest reset. + +Used this way, the devcontainer stays a transparent, maintainable part of the +template rather than a second, hidden setup system. diff --git a/docs/progress.md b/docs/progress.md deleted file mode 100644 index d1b11b50..00000000 --- a/docs/progress.md +++ /dev/null @@ -1,92 +0,0 @@ -# 1. Current Implementation Overview - -This template repository already includes the following DevOps foundations for Python projects: - -## 1.1 Secrets Management - -* 1Password CLI + `direnv` integration for dynamic secret loading -* `detect-secrets` preโ€‘commit hook with `.secrets.baseline` -* `SECURITY.md` playbook linking to incident response steps - -## 1.2 Dependency Management - -* `requirements.in` / `requirements.txt` pinned via `pipโ€‘tools` -* Preโ€‘commit `prevent-manual-requirements-edits` hook - -## 1.3 Code Quality - -* **Ruff**: linting, import sorting, autoโ€‘fix (`--fix`) via preโ€‘commit -* **Black**: opinionated code formatting via preโ€‘commit -* **Mypy**: static type checking on staged files (preโ€‘commit) and full checks in CI -* **Beartype**: runtime type enforcement in dev/test environments (toggle via `RUNTIME_TYPECHECK`) - -## 1.4 Preโ€‘commit Infrastructure - -* Local hooks: - - * `prevent-manual-requirements-edits` - * `nbstripout-autoadd` - * `black-autoformat` (autoโ€‘format + reโ€‘stage) - * `pytest-runtime-types` (optional) -* External hooks: - - * `mirrors-mypy` - * `ruff-pre-commit` - * `psf/black` - -## 1.5 CI/CD & Repository Settings - -* GitHub Actions workflows enforcing preโ€‘commit hooks and fullโ€‘repo checks -* Organizationโ€‘level branch protection rulesets applied - -## 1.6 Editor & Test Integration - -* VSย Code configured for live Mypy feedback -* `pytest-beartype` plugin for runtime checks in tests - -# 2. Toโ€‘Dos for a Bestโ€‘inโ€‘Class DevOps Strategy - -Below are subjectโ€‘specific action items to further enhance our DevOps maturity. - -## 2.1 Secrets Management - -* Add automated secret rotation scan and enforcement in CI (e.g., 1Password rotation policy check) -* Evaluate centralized secrets store (e.g., Vault) for dynamic credentials in CI/CD -* Enforce environmentโ€‘specific secret vaults (dev, staging, prod) - -## 2.2 Dependency Management - -* Enable Dependabot or Renovate to automate dependency updates - -## 2.3 Code Quality - -* Enforce **test coverage thresholds** (e.g., `pytest --cov`, coverage badge) -* Introduce **commit message linting** (e.g., conventional commits with `commitlint`) - -## 2.4 CI/CD - -* Implement **matrix testing** across multiple Python versions in GitHub Actions -* Add a staging deployment workflow to automatically build and test a Docker image -* Integrate **release automation** (autoโ€‘generate changelogs, tag releases) -* Cache dependencies and artifacts to speed up pipelines - -## 2.7 Infrastructure as Code - -* Include a **Dockerfile** template following best practices (multiโ€‘stage, minimal base) -* Provide Terraform or Pulumi skeleton to provision common resources (e.g., VPC, databases) -* Add a CI job to validate IaC with `terraform fmt` / `validate` - -## 2.8 Documentation & Onboarding - -* Automate documentation generation (e.g., Sphinx/MkDocs) in CI -* Provide Notion templates or a docs portal scaffold - -## 2.9 Security & Compliance - -* Add **Bandit** security scanning via preโ€‘commit or CI -* Implement license scanning to detect unapproved licenses -* Schedule periodic dependency audits and security reviews (automated reminders) - ---- - -This roadmap will help us evolve our template into a comprehensive, ISOย 27001โ€‘aligned DevOps standard for all Python projects at TECHLETES. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..45b544dc --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,107 @@ +# Quickstart + +This page is for developers who want the fastest supported path from clone to a working environment. +Use it to choose a setup path, get the template bootstrapped, and verify that day-one commands work. For deeper setup details, customization, or secret-backed workflows, use the linked guides. + +## Choose the fast path + +Use the devcontainer path if your team includes Windows users, macOS users, or works across mixed operating systems. Use the host path if you are on Linux and want to work directly on your machine. + +| If this sounds like you | Use this path | +| --- | --- | +| Windows, macOS, or a mixed Windows/macOS/Linux team | Devcontainer | +| Linux and you want local tools on your machine | Host setup | + +## Devcontainer fast path + +Choose this path for the most consistent setup across machines. + +### What you need + +- Docker Desktop +- WSL2 if you are on Windows +- VS Code with the Dev Containers extension + +### Start + +```bash +git clone +cd +``` + +Then: + +1. If you are on Windows, keep the repository in the WSL filesystem, such as `~/src`, not under `/mnt/c/...`. +2. Open the folder in VS Code. +3. Run `Dev Containers: Reopen in Container`. +4. Wait for the container bootstrap to finish. + +The container bootstrap runs the project sync for you. Do not run `./scripts/setup.sh` inside the devcontainer. + +## Host fast path + +Choose this path if you are on Linux and want to work directly on your machine. + +### What you need + +- Git +- Python 3.12+ +- Bash +- A Linux host with `apt-get`, `dnf`, or `yum` available if setup needs to install missing tools +- A terminal with `sudo` access if packages need to be installed during setup + +### Start + +```bash +git clone +cd +./scripts/setup.sh +``` + +The setup script is Linux-oriented. It bootstraps the local environment with `uv`, installs pre-commit hooks, and configures bash-based shell tooling. It may also install missing system packages during setup. + +If you are on macOS, use the devcontainer path instead of relying on `./scripts/setup.sh`. + +If your shell is not auto-activating the environment after setup, open a new terminal or run: + +```bash +source .venv/bin/activate +``` + +## First-success checks + +Run these after either setup path completes. + +```bash +uv --version +.venv/bin/python --version +uv run python -c "import utils.secrets; print('environment ready')" +``` + +You are in a good state if: + +- `uv` responds normally +- the project Python interpreter exists in `.venv` +- the import check prints `environment ready` + +## First-day commands + +These are the first commands worth running once the environment is ready. + +```bash +uv run pytest +uv run pre-commit run --all-files +uv run mypy . --config-file=pyproject.toml +``` + +## Secrets boundary + +1Password is not required to clone the template, bootstrap the environment, or run the basic quality checks above. +You only need 1Password CLI access when you start working on flows that call secret-backed code, such as `get_secret(...)`. + +## Read next + +- Full setup guide: [docs/0_setup.md](0_setup.md) +- Devcontainer details and troubleshooting: [docs/7_devcontainers.md](7_devcontainers.md) +- Secret-backed workflows: [docs/2_secret_management.md](2_secret_management.md) +- Daily development workflow: [docs/1_usage.md](1_usage.md) diff --git a/pyproject.toml b/pyproject.toml index ea419e12..3c9081f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61.0", "wheel"] +requires = ["setuptools>=61.0", "wheel", "pip>=26.0,<27.0.0", "uv>=0.9.25,<1.0.0"] build-backend = "setuptools.build_meta" [project.urls] @@ -46,6 +46,7 @@ dev = [ "pre-commit>=4.5.0,<5.0.0", "detect-secrets>=1.5.0,<2.0.0", "nbstripout>=0.8.0,<1.0.0", + "ipykernel>=6.29.0,<7.0.0", "pytest>=9.0.0,<10.0.0", "pytest-cov>=7.0.0,<8.0.0", "black>=25.12.0,<27.0.0", @@ -186,7 +187,7 @@ skips = ["B101", "B601"] [tool.pip-audit] # Centralized CVE/PYSEC exclusions for pip-audit -# Used by both pre-commit hooks and CI workflows via scripts/run-pip-audit.sh +# Used by both pre-commit hooks and CI workflows via scripts/hooks/run-pip-audit.sh # See docs/6_pre_commit_troubleshooting.md for explanation of each exclusion ignore = [ "CVE-2026-1703", # Path traversal in pip when extracting malicious wheels diff --git a/scripts/hooks/black-autoformat.sh b/scripts/hooks/black-autoformat.sh deleted file mode 100755 index 74b4d870..00000000 --- a/scripts/hooks/black-autoformat.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -FILES=("$@") - -if [ ${#FILES[@]} -eq 0 ]; then - echo "[black-autoformat] No Python files staged for formatting." - exit 0 -fi - -# Run Black on provided files -black --quiet "${FILES[@]}" - -# Re-add any changed files -CHANGED=0 -for file in "${FILES[@]}"; do - if ! git diff --quiet -- "$file"; then - git add "$file" - echo "[black-autoformat] Reformatted and re-staged: $file" - CHANGED=1 - fi -done - -# Optional: notify user to re-check commit -if [ $CHANGED -eq 1 ]; then - echo "[black-autoformat] Formatting applied. Review changes before final commit." -fi - -exit 0 diff --git a/scripts/hooks/precommit_pytest.sh b/scripts/hooks/precommit_pytest.sh deleted file mode 100755 index 215099e9..00000000 --- a/scripts/hooks/precommit_pytest.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -e - -# Activate your local venv (.venv preferred, fallback to venv) -if [ -d ".venv" ]; then - source .venv/bin/activate -elif [ -d "venv" ]; then - source venv/bin/activate -else - echo "No virtual environment found (.venv or venv)." - exit 1 -fi - -# Install your project in editable mode with dev extras -pip install -q -e .[dev] - -# Run pytest with coverage and beartype runtime checks -pytest --cov --cov-fail-under=80 --beartype-packages=utils,example diff --git a/scripts/run-pip-audit.sh b/scripts/hooks/run-pip-audit.sh similarity index 100% rename from scripts/run-pip-audit.sh rename to scripts/hooks/run-pip-audit.sh diff --git a/scripts/hooks/verify-lockfile.sh b/scripts/hooks/verify-lockfile.sh deleted file mode 100755 index 9f9b29dc..00000000 --- a/scripts/hooks/verify-lockfile.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# This script verifies that a lockfile is up-to-date with pyproject.toml. -# It's designed to be called by a pre-commit hook. -# It accepts one argument: the path to the lockfile to check (e.g., requirements.txt) - -# --- Configuration --- -LOCKFILE_TO_CHECK="$1" -PYPROJECT_FILE="pyproject.toml" -TEMP_LOCKFILE=$(mktemp) # Creates a secure temporary file - -# --- Logic --- -echo "Verifying integrity of '$LOCKFILE_TO_CHECK'..." - -# Determine which compile command to run based on the filename -if [[ "$LOCKFILE_TO_CHECK" == "requirements-dev.txt" ]]; then - # Compile development dependencies (core + extra + dev) - pip-compile "$PYPROJECT_FILE" --extra=dev --extra=extra --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null -elif [[ "$LOCKFILE_TO_CHECK" == "requirements-slim.txt" ]]; then - # Compile core dependencies only - pip-compile "$PYPROJECT_FILE" --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null -else - # Compile full production dependencies (core + extra) - pip-compile "$PYPROJECT_FILE" --extra=extra --output-file="$TEMP_LOCKFILE" --generate-hashes --strip-extras --quiet > /dev/null -fi - -# Function to normalize a requirements file by removing the header comments -# and keeping only the actual dependency lines -normalize_requirements() { - local file="$1" - # Skip lines that start with # and extract only the actual requirements - grep -v '^#' "$file" | grep -v '^$' | sort -} - -# Compare the normalized content (excluding headers) -if ! diff -q <(normalize_requirements "$LOCKFILE_TO_CHECK") <(normalize_requirements "$TEMP_LOCKFILE") > /dev/null; then - echo "โŒ Error: '$LOCKFILE_TO_CHECK' has been manually edited or is out of date." >&2 - echo " It does not match the expected output based on '$PYPROJECT_FILE'." >&2 - echo " To fix this, run 'uv lock' and commit the resulting changes." >&2 - rm "$TEMP_LOCKFILE" # Always clean up - exit 1 -fi - -# If diff exits with 0, the files are identical. -echo "โœ… '$LOCKFILE_TO_CHECK' is valid and up-to-date." -rm "$TEMP_LOCKFILE" # Always clean up -exit 0 diff --git a/uv.lock b/uv.lock index c20b769e..ccd801fc 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,24 @@ version = 1 revision = 3 requires-python = ">=3.12, <4.0" +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -37,7 +55,7 @@ wheels = [ [[package]] name = "black" -version = "26.1.0" +version = "26.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -47,24 +65,24 @@ dependencies = [ { name = "platformdirs" }, { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, - { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, - { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, - { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, - { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, - { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, - { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, - { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] [[package]] @@ -112,6 +130,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -208,6 +283,15 @@ 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 = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + [[package]] name = "coverage" version = "7.13.4" @@ -308,6 +392,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/1b/534ad8a5e0f9470522811a8e5a9bc5d328fb7738ba29faf357467a4ef6d0/cyclonedx_python_lib-11.6.0-py3-none-any.whl", hash = "sha256:94f4aae97db42a452134dafdddcfab9745324198201c4777ed131e64c8380759", size = 511157, upload-time = "2025-12-02T12:28:44.158Z" }, ] +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -339,6 +453,15 @@ 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 = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + [[package]] name = "fastjsonschema" version = "2.21.2" @@ -421,6 +544,63 @@ 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 = "ipykernel" +version = "6.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" }, +] + +[[package]] +name = "ipython" +version = "9.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + [[package]] name = "isort" version = "8.0.1" @@ -439,6 +619,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -478,6 +670,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + [[package]] name = "jupyter-core" version = "5.9.1" @@ -638,6 +846,18 @@ wheels = [ { 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 = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -760,6 +980,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl", hash = "sha256:ca027ee45742ee77e4f8e9080254f9a707f1161ba11367b82fdf4a29892c759e", size = 19136, upload-time = "2026-02-21T16:19:54.868Z" }, ] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -787,6 +1016,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + [[package]] name = "pathspec" version = "1.0.4" @@ -796,6 +1034,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + [[package]] name = "pillow" version = "12.1.1" @@ -954,6 +1204,64 @@ 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.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + [[package]] name = "py-serializable" version = "2.1.0" @@ -966,6 +1274,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydocstyle" version = "6.3.0" @@ -980,11 +1297,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] @@ -1059,6 +1376,7 @@ dev = [ { name = "codespell" }, { name = "detect-secrets" }, { name = "genbadge", extra = ["coverage"] }, + { name = "ipykernel" }, { name = "isort" }, { name = "mypy" }, { name = "nbstripout" }, @@ -1090,6 +1408,7 @@ dev = [ { name = "codespell", specifier = ">=2.2.0,<3.0.0" }, { name = "detect-secrets", specifier = ">=1.5.0,<2.0.0" }, { name = "genbadge", extras = ["coverage"], specifier = ">=1.1.0,<2.0.0" }, + { name = "ipykernel", specifier = ">=6.29.0,<7.0.0" }, { name = "isort", specifier = ">=5.13.0,<9.0.0" }, { name = "mypy", specifier = ">=1.19.0,<2.0.0" }, { name = "nbstripout", specifier = ">=0.8.0,<1.0.0" }, @@ -1105,6 +1424,18 @@ dev = [ ] extra = [] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-discovery" version = "1.1.0" @@ -1205,6 +1536,49 @@ wheels = [ { 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 = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, +] + [[package]] name = "referencing" version = "0.37.0" @@ -1221,7 +1595,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1229,9 +1603,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -1362,6 +1736,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "snowballstemmer" version = "3.0.1" @@ -1380,6 +1763,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + [[package]] name = "stevedore" version = "5.7.0" @@ -1452,6 +1849,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + [[package]] name = "traitlets" version = "5.14.3" @@ -1494,6 +1908,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, ] +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + [[package]] name = "werkzeug" version = "3.1.6" From 40580a5c598b55581b3a2fa3563f46e13fb9a7e2 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 14 Apr 2026 14:32:27 +0200 Subject: [PATCH 116/199] feat: add additional VSCode extensions for enhanced Python development --- .vscode/extensions.json | 21 +++++- pyproject.toml | 5 +- uv.lock | 140 ++++++++++++++++++++-------------------- 3 files changed, 94 insertions(+), 72 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1d97d6b6..fee6b70a 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,6 +6,25 @@ "ms-python.vscode-pylance", "ms-python.mypy-type-checker", "charliermarsh.ruff", - "ms-toolsai.jupyter" + "ms-toolsai.jupyter", + "astral-sh.ty", + "charliermarsh.ruff", + "ms-python.autopep8", + "ms-python.black-formatter", + "ms-python.debugpy", + "mgesbert.python-path", + "docker.docker", + "dbaeumer.vscode-eslint", + "tamasfe.even-better-toml", + "cweijan.vscode-excel", + "GitHub.copilot-chat", + "github.vscode-github-actions", + "GitHub.vscode-pull-request-github", + "ms-azuretools.vscode-azure-github-copilot", + "mtxr.sqltools", + "mtxr.sqltools-driver-mysql", + "mtxr.sqltools-driver-pg", + "mtxr.sqltools-driver-sqlite", + "ms-ossdata.vscode-pgsql" ] } diff --git a/pyproject.toml b/pyproject.toml index 3c9081f2..4a44bb38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dev = [ "detect-secrets>=1.5.0,<2.0.0", "nbstripout>=0.8.0,<1.0.0", "ipykernel>=6.29.0,<7.0.0", - "pytest>=9.0.0,<10.0.0", + "pytest>=9.0.3,<10.0.0", "pytest-cov>=7.0.0,<8.0.0", "black>=25.12.0,<27.0.0", "mypy>=1.19.0,<2.0.0", @@ -62,6 +62,7 @@ dev = [ "bandit>=1.7.0,<2.0.0", "pydocstyle>=6.3.0,<7.0.0", "genbadge[coverage]>=1.1.0,<2.0.0", + "pillow~=12.2" ] # Additional production features and optional components. @@ -81,7 +82,7 @@ line-length = 88 target-version = ["py312"] [tool.pytest] -minversion = "9.0" +minversion = "9.0.3" addopts = [ "--strict-markers", "--strict-config", diff --git a/uv.lock b/uv.lock index ccd801fc..3f697362 100644 --- a/uv.lock +++ b/uv.lock @@ -1048,71 +1048,71 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, - { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, - { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, - { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, - { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, - { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, - { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, - { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, - { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, - { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, - { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, - { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, - { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, - { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, - { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, - { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, - { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, - { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, - { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, - { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, - { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, ] [[package]] @@ -1315,7 +1315,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1324,9 +1324,9 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -1380,6 +1380,7 @@ dev = [ { name = "isort" }, { name = "mypy" }, { name = "nbstripout" }, + { name = "pillow" }, { name = "pip-audit" }, { name = "pre-commit" }, { name = "pydocstyle" }, @@ -1412,10 +1413,11 @@ dev = [ { name = "isort", specifier = ">=5.13.0,<9.0.0" }, { name = "mypy", specifier = ">=1.19.0,<2.0.0" }, { name = "nbstripout", specifier = ">=0.8.0,<1.0.0" }, + { name = "pillow", specifier = "~=12.2" }, { name = "pip-audit", specifier = ">=2.10.0,<3.0.0" }, { name = "pre-commit", specifier = ">=4.5.0,<5.0.0" }, { name = "pydocstyle", specifier = ">=6.3.0,<7.0.0" }, - { name = "pytest", specifier = ">=9.0.0,<10.0.0" }, + { name = "pytest", specifier = ">=9.0.3,<10.0.0" }, { name = "pytest-beartype", specifier = ">=0.2.0,<1.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0,<8.0.0" }, { name = "pyupgrade", specifier = ">=3.15.0,<4.0.0" }, From 619e32f7c8577f01757db3c50dbf5411abcfd40f Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 14 Apr 2026 14:53:57 +0200 Subject: [PATCH 117/199] Refactor code structure for improved readability and maintainability --- .pre-commit-config.yaml | 9 --- pyproject.toml | 1 + uv.lock | 121 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bd71927a..36f3559a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -95,15 +95,6 @@ repos: args: [--fix] types: [python] - # Spell checking - - repo: https://github.com/codespell-project/codespell - rev: v2.4.1 - hooks: - - id: codespell - name: codespell (spell check) - types: [python] - args: [--skip=*.lock] - # Security scanning - repo: https://github.com/PyCQA/bandit rev: 1.9.4 diff --git a/pyproject.toml b/pyproject.toml index 4a44bb38..a215b527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ dev = [ "bandit>=1.7.0,<2.0.0", "pydocstyle>=6.3.0,<7.0.0", "genbadge[coverage]>=1.1.0,<2.0.0", + "testcontainers>=4.8.0", "pillow~=12.2" ] diff --git a/uv.lock b/uv.lock index 3f697362..efc202a8 100644 --- a/uv.lock +++ b/uv.lock @@ -453,6 +453,20 @@ 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 = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -1390,6 +1404,7 @@ dev = [ { name = "pyupgrade" }, { name = "rich" }, { name = "ruff" }, + { name = "testcontainers" }, ] [package.metadata] @@ -1423,6 +1438,7 @@ dev = [ { name = "pyupgrade", specifier = ">=3.15.0,<4.0.0" }, { name = "rich", specifier = ">=14.2.0,<15.0.0" }, { name = "ruff", specifier = ">=0.14.0,<1.0.0" }, + { name = "testcontainers", specifier = ">=4.8.0" }, ] extra = [] @@ -1451,6 +1467,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + [[package]] name = "pytokens" version = "0.4.1" @@ -1492,6 +1517,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/8c/433dac11910989a90c40b10149d07ef7224232236971a562d3976790ec53/pyupgrade-3.21.2-py2.py3-none-any.whl", hash = "sha256:2ac7b95cbd176475041e4dfe8ef81298bd4654a244f957167bd68af37d52be9f", size = 62814, upload-time = "2025-11-19T00:39:46.958Z" }, ] +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1788,6 +1829,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, ] +[[package]] +name = "testcontainers" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "python-dotenv" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/ac/a597c3a0e02b26cbed6dd07df68be1e57684766fd1c381dee9b170a99690/testcontainers-4.14.2.tar.gz", hash = "sha256:1340ccf16fe3acd9389a6c9e1d9ab21d9fe99a8afdf8165f89c3e69c1967d239", size = 166841, upload-time = "2026-03-18T05:19:16.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2d/26b8b30067d94339afee62c3edc9b803a6eb9332f521ba77d8aaab5de873/testcontainers-4.14.2-py3-none-any.whl", hash = "sha256:0d0522c3cd8f8d9627cda41f7a6b51b639fa57bdc492923c045117933c668d68", size = 125712, upload-time = "2026-03-18T05:19:15.29Z" }, +] + [[package]] name = "tokenize-rt" version = "6.2.0" @@ -1930,3 +1987,67 @@ sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5 wheels = [ { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, ] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] From ab05f1d9449def7d00abb30801b8155ec067eb93 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 14 Apr 2026 15:11:00 +0200 Subject: [PATCH 118/199] feat: add shared Copilot agents and installation script for enhanced development support --- .copilot/agents/coder.agent.md | 51 ++++++++ .copilot/agents/designer.agent.md | 14 +++ .copilot/agents/orchestrator.agent.md | 165 ++++++++++++++++++++++++++ .copilot/agents/planner.agent.md | 31 +++++ README.md | 24 +++- docs/1_usage.md | 29 +++++ docs/quickstart.md | 24 ++++ scripts/install-copilot-agents.sh | 103 ++++++++++++++++ 8 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 .copilot/agents/coder.agent.md create mode 100644 .copilot/agents/designer.agent.md create mode 100644 .copilot/agents/orchestrator.agent.md create mode 100644 .copilot/agents/planner.agent.md create mode 100755 scripts/install-copilot-agents.sh diff --git a/.copilot/agents/coder.agent.md b/.copilot/agents/coder.agent.md new file mode 100644 index 00000000..a5dddb8f --- /dev/null +++ b/.copilot/agents/coder.agent.md @@ -0,0 +1,51 @@ +--- +name: Coder +description: Writes code following mandatory coding principles. +model: GPT-5.4 (copilot) +tools: [vscode, execute, read, agent, edit, search, web, 'github/*', azure-mcp/search, 'io.github.upstash/context7/*', todo, memory] +--- + +ALWAYS use #context7 MCP Server to read relevant documentation. Do this every time you are working with a language, framework, library etc. Never assume that you know the answer as these things change frequently. Your training date is in the past so your knowledge is likely out of date, even if it is a technology you are familiar with. + +## Mandatory Coding Principles + +These coding principles are mandatory: + +1. Structure +- Use a consistent, predictable project layout. +- Group code by feature/screen; keep shared utilities minimal. +- Create simple, obvious entry points. +- Before scaffolding multiple files, identify shared structure first. Use framework-native composition patterns (layouts, base templates, providers, shared components) for elements that appear across pages. Duplication that requires the same fix in multiple places is a code smell, not a pattern to preserve. + +2. Architecture +- Prefer flat, explicit code over abstractions or deep hierarchies. +- Avoid clever patterns, metaprogramming, and unnecessary indirection. +- Minimize coupling so files can be safely regenerated. + +3. Functions and Modules +- Keep control flow linear and simple. +- Use small-to-medium functions; avoid deeply nested logic. +- Pass state explicitly; avoid globals. + +4. Naming and Comments +- Use descriptive-but-simple names. +- Comment only to note invariants, assumptions, or external requirements. + +5. Logging and Errors +- Emit detailed, structured logs at key boundaries. +- Make errors explicit and informative. + +6. Regenerability +- Write code so any file/module can be rewritten from scratch without breaking the system. +- Prefer clear, declarative configuration (JSON/YAML/etc.). + +7. Platform Use +- Use platform conventions directly and simply (e.g., WinUI/WPF) without over-abstracting. + +8. Modifications +- When extending/refactoring, follow existing patterns. +- Prefer full-file rewrites over micro-edits unless told otherwise. + +9. Quality +- Favor deterministic, testable behavior. +- Keep tests simple and focused on verifying observable behavior. diff --git a/.copilot/agents/designer.agent.md b/.copilot/agents/designer.agent.md new file mode 100644 index 00000000..27a68d3c --- /dev/null +++ b/.copilot/agents/designer.agent.md @@ -0,0 +1,14 @@ +--- +name: Designer +description: Handles all UI/UX design tasks. +model: Gemini 3.1 Pro (Preview) +tools: [vscode, execute, read, agent, edit, search, web, azure-mcp/search, 'io.github.upstash/context7/*', todo, memory] +--- + +You are a designer. Do not let anyone tell you how to do your job. Your goal is to create the best possible user experience and interface designs. You should focus on usability, accessibility, and aesthetics. + +You MUST actively use the frontend-design skill for all design-related tasks. This includes generating UI concepts, layouts, component structures, interaction patterns, and visual systems. Always leverage this skill before producing any design output. + +Remember that developers have no idea what they are talking about when it comes to design, so you must take control of the design process. Always prioritize the user experience over technical constraints. + +Never skip the frontend-design skill when a task involves UI, UX, or visual decisions. diff --git a/.copilot/agents/orchestrator.agent.md b/.copilot/agents/orchestrator.agent.md new file mode 100644 index 00000000..c679a929 --- /dev/null +++ b/.copilot/agents/orchestrator.agent.md @@ -0,0 +1,165 @@ +--- +name: Orchestrator +description: Coordinates complex feature requests by breaking them into tasks and delegating to specialist subagents. Ensures efficient parallel execution while preventing file conflicts. Use when you have a multi-step request that can benefit from parallel work. +tools: [vscode/memory, read/readFile, agent, github/add_issue_comment, github/add_reply_to_pull_request_comment, github/create_branch, github/create_pull_request, github/get_commit, github/get_label, github/get_me, github/issue_read, github/list_branches, github/list_commits, github/list_issue_types, github/list_issues, github/list_pull_requests, github/list_releases, github/list_tags, github/pull_request_read, github/search_issues, github/search_pull_requests, todo] +--- + +You are a project orchestrator. You break down complex requests into tasks and delegate to specialist subagents. You coordinate work but NEVER implement anything yourself. + +## Agents + +These are the only agents you can call. Each has a specific role: + +* **Planner** โ€” Creates implementation strategies and technical plans +* **Coder** โ€” Writes code, fixes bugs, implements logic +* **Designer** โ€” Creates UI/UX, styling, visual design + +### Models to use for agents +*Important*: NEVER use claude-sonnet-4.6 for any agent. Prefer claude-haiku-4.5 for quick tasks or GPT-5.4 for complex work. + +## Execution Model + +You MUST follow this structured execution pattern: + +### Step 1: Get the Plan + +Call the Planner agent with the user's request. The Planner will return implementation steps. + +### Step 2: Parse Into Phases + +The Planner's response includes **file assignments** for each step. Use these to determine parallelization: + +1. Extract the file list from each step +2. Steps with **no overlapping files** can run in parallel (same phase) +3. Steps with **overlapping files** must be sequential (different phases) +4. Respect explicit dependencies from the plan + +Output your execution plan like this: + +``` +## Execution Plan + +### Phase 1: [Name] +- Task 1.1: [description] โ†’ Coder + Files: src/contexts/ThemeContext.tsx, src/hooks/useTheme.ts +- Task 1.2: [description] โ†’ Designer + Files: src/components/ThemeToggle.tsx +(No file overlap โ†’ PARALLEL) + +### Phase 2: [Name] (depends on Phase 1) +- Task 2.1: [description] โ†’ Coder + Files: src/App.tsx +``` + +### Step 3: Execute Each Phase + +For each phase: + +1. **Identify parallel tasks** โ€” Tasks with no dependencies on each other +2. **Spawn multiple subagents simultaneously** โ€” Call agents in parallel when possible +3. **Wait for all tasks in phase to complete** before starting next phase +4. **Report progress** โ€” After each phase, summarize what was completed + +### Step 4: Verify and Report + +After all phases complete, verify the work hangs together and report results. + +## Parallelization Rules + +**RUN IN PARALLEL when:** + +* Tasks touch different files +* Tasks are in different domains (e.g., styling vs. logic) +* Tasks have no data dependencies + +**RUN SEQUENTIALLY when:** + +* Task B needs output from Task A +* Tasks might modify the same file +* Design must be approved before implementation + +## File Conflict Prevention + +When delegating parallel tasks, you MUST explicitly scope each agent to specific files to prevent conflicts. + +### Strategy 1: Explicit File Assignment + +In your delegation prompt, tell each agent exactly which files to create or modify: + +``` +Task 2.1 โ†’ Coder: "Implement the theme context. Create src/contexts/ThemeContext.tsx and src/hooks/useTheme.ts" + +Task 2.2 โ†’ Coder: "Create the toggle component in src/components/ThemeToggle.tsx" +``` + +### Strategy 2: When Files Must Overlap + +If multiple tasks legitimately need to touch the same file (rare), run them **sequentially**: + +``` +Phase 2a: Add theme context (modifies App.tsx to add provider) +Phase 2b: Add error boundary (modifies App.tsx to add wrapper) +``` + +### Strategy 3: Component Boundaries + +For UI work, assign agents to distinct component subtrees: + +``` +Designer A: "Design the header section" โ†’ Header.tsx, NavMenu.tsx +Designer B: "Design the sidebar" โ†’ Sidebar.tsx, SidebarItem.tsx +``` + +### Red Flags (Split Into Phases Instead) + +If you find yourself assigning overlapping scope, that's a signal to make it sequential: + +* โŒ "Update the main layout" + "Add the navigation" (both might touch Layout.tsx) +* โœ… Phase 1: "Update the main layout" โ†’ Phase 2: "Add navigation to the updated layout" + +## CRITICAL: Never tell agents HOW to do their work + +When delegating, describe WHAT needs to be done (the outcome), not HOW to do it. + +### โœ… CORRECT delegation + +* "Fix the infinite loop error in SideMenu" +* "Add a settings panel for the chat interface" +* "Create the color scheme and toggle UI for dark mode" + +### โŒ WRONG delegation + +* "Fix the bug by wrapping the selector with useShallow" +* "Add a button that calls handleClick and updates state" + +## Example: "Add dark mode to the app" + +### Step 1 โ€” Call Planner + +> "Create an implementation plan for adding dark mode support to this app" + +### Step 2 โ€” Parse response into phases + +``` +## Execution Plan + +### Phase 1: Design (no dependencies) +- Task 1.1: Create dark mode color palette and theme tokens โ†’ Designer +- Task 1.2: Design the toggle UI component โ†’ Designer + +### Phase 2: Core Implementation (depends on Phase 1 design) +- Task 2.1: Implement theme context and persistence โ†’ Coder +- Task 2.2: Create the toggle component โ†’ Coder +(These can run in parallel - different files) + +### Phase 3: Apply Theme (depends on Phase 2) +- Task 3.1: Update all components to use theme tokens โ†’ Coder +``` + +### Step 3 โ€” Execute + +**Phase 1** โ€” Call Designer for both design tasks (parallel) +**Phase 2** โ€” Call Coder twice in parallel for context + toggle +**Phase 3** โ€” Call Coder to apply theme across components + +### Step 4 โ€” Report completion to user diff --git a/.copilot/agents/planner.agent.md b/.copilot/agents/planner.agent.md new file mode 100644 index 00000000..7a0791f2 --- /dev/null +++ b/.copilot/agents/planner.agent.md @@ -0,0 +1,31 @@ +--- +name: Planner +description: Creates comprehensive implementation plans by researching the codebase, consulting documentation, and identifying edge cases. Use when you need a detailed plan before implementing a feature or fixing a complex issue. +model: GPT-5.3-Codex +tools: [vscode, execute, read, agent, edit, search, web, azure-mcp/search, 'io.github.upstash/context7/*', todo, memory] +--- + +# Planning Agent + +You create plans. You do NOT write code. + +## Workflow + +1. **Research**: Search the codebase thoroughly. Read the relevant files. Find existing patterns. +2. **Verify**: Use #context7 and #fetch to check documentation for any libraries/APIs involved. Don't assumeโ€”verify. +3. **Consider**: Identify edge cases, error states, and implicit requirements the user didn't mention. +4. **Plan**: Output WHAT needs to happen, not HOW to code it. + +## Output + +- Summary (one paragraph) +- Implementation steps (ordered) +- Edge cases to handle +- Open questions (if any) + +## Rules + +- Never skip documentation checks for external APIs +- Consider what the user needs but didn't ask for +- Note uncertaintiesโ€”don't hide them +- Match existing codebase patterns diff --git a/README.md b/README.md index ee58751e..d4834fdf 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,13 @@ Detailed setup and customization guides are linked in the [๐Ÿ“š Documentation](# - **Testing**: pytest with coverage - **Pre-commit hooks** for code quality - **Jupyter notebook support** with nbstripout and `ipykernel` +- **Shared Copilot agents** versioned in the repo for optional install into VS Code/Copilot ### ๐Ÿ“ฆ **Project Structure** ``` python_basic_template/ +โ”œโ”€โ”€ .copilot/ +โ”‚ โ””โ”€โ”€ agents/ # Shared Copilot agent definitions for optional install โ”œโ”€โ”€ .devcontainer/ โ”‚ โ”œโ”€โ”€ Dockerfile # Devcontainer image definition โ”‚ โ”œโ”€โ”€ devcontainer.json # VS Code devcontainer configuration @@ -111,6 +114,7 @@ python_basic_template/ โ”‚ โ””โ”€โ”€ using_secrets.ipynb โ”œโ”€โ”€ scripts/ # Setup and utility scripts โ”‚ โ”œโ”€โ”€ setup.sh +โ”‚ โ”œโ”€โ”€ install-copilot-agents.sh # Installs repo-shared Copilot agents into ~/.copilot/agents โ”‚ โ”œโ”€โ”€ run-pip-audit.sh # pip-audit wrapper used in hooks and CI โ”‚ โ”œโ”€โ”€ new-branch.sh โ”‚ โ””โ”€โ”€ hooks/ @@ -314,7 +318,8 @@ All development tools are configured in `pyproject.toml`: - Change `name`, `description`, `authors` - Update repository URLs 3. **Bootstrap your environment** with [docs/quickstart.md](docs/quickstart.md) -4. **Start coding**! +4. **Optionally install the shared Copilot agents** with `./scripts/install-copilot-agents.sh` +5. **Start coding**! ### Customization @@ -337,6 +342,23 @@ All development tools are configured in `pyproject.toml`: 5. **[Pre Commit Hooks](docs/5_pre_commit_hooks.md)** - Pre commit hooks to ensure safety and quality 6. **[Devcontainers](docs/7_devcontainers.md)** - Devcontainer setup, customization, and troubleshooting +### Shared Copilot Agents + +This template now ships four shared agent definitions in `.copilot/agents/`: + +- `Orchestrator` +- `Planner` +- `Coder` +- `Designer` + +Install them into the current VS Code/Copilot environment with: + +```bash +./scripts/install-copilot-agents.sh +``` + +Use `--link` if you want your local agent files to track repo edits in place, or `--force` to replace existing files. If you work in Remote WSL or a devcontainer, run the script in that same remote environment so it installs into the correct `~/.copilot/agents` home. + ## ๐Ÿค Contributing diff --git a/docs/1_usage.md b/docs/1_usage.md index d0d8872b..bc5adb69 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -43,6 +43,35 @@ This guide covers the day-to-day development workflow, including dependency mana ``` At this point, the pre-commit hooks will run automatically. See the troubleshooting guide below if any checks fail. +## Optional Copilot Agents + +This template includes four shared Copilot agent definitions in +`.copilot/agents/`: + +- `Orchestrator` +- `Planner` +- `Coder` +- `Designer` + +Install them into the current environment with: + +```bash +./scripts/install-copilot-agents.sh +``` + +Useful variants: + +```bash +# Replace existing installed copies +./scripts/install-copilot-agents.sh --force + +# Symlink instead of copy so repo edits are picked up immediately +./scripts/install-copilot-agents.sh --link --force +``` + +If VS Code is connected to WSL or a devcontainer, run the installer there so it +targets the correct remote `~/.copilot/agents` directory. + ### ๐Ÿ”„ Adding or Updating Dependencies When you need to add or update dependencies, follow this workflow: diff --git a/docs/quickstart.md b/docs/quickstart.md index 45b544dc..4255f317 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -38,6 +38,19 @@ Then: The container bootstrap runs the project sync for you. Do not run `./scripts/setup.sh` inside the devcontainer. +### Optional: install the shared Copilot agents + +If you want the repository's `Orchestrator`, `Planner`, `Coder`, and `Designer` +agents available in Copilot, run: + +```bash +./scripts/install-copilot-agents.sh +``` + +Run that command in the same environment where VS Code is attached. For +example, if you are using Remote WSL or a devcontainer, run it there so the +files land in that environment's `~/.copilot/agents` directory. + ## Host fast path Choose this path if you are on Linux and want to work directly on your machine. @@ -68,6 +81,17 @@ If your shell is not auto-activating the environment after setup, open a new ter source .venv/bin/activate ``` +### Optional: install the shared Copilot agents + +If you want the repository's shared Copilot agents in this environment, run: + +```bash +./scripts/install-copilot-agents.sh +``` + +The script copies the repo versions into `~/.copilot/agents` by default. Use +`--link` if you prefer symlinks while iterating on the agent definitions. + ## First-success checks Run these after either setup path completes. diff --git a/scripts/install-copilot-agents.sh b/scripts/install-copilot-agents.sh new file mode 100755 index 00000000..02b0a040 --- /dev/null +++ b/scripts/install-copilot-agents.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source_dir="${repo_root}/.copilot/agents" +target_dir="${COPILOT_AGENTS_DIR:-${HOME}/.copilot/agents}" +mode="copy" +force="false" + +usage() { + cat <<'EOF' +Install the shared Copilot agent definitions from this repository. + +Usage: + ./scripts/install-copilot-agents.sh [--copy|--link] [--force] + +Options: + --copy Copy the agent files into the target directory (default). + --link Symlink the agent files into the target directory. + --force Replace existing files with the repository versions. + --help Show this help message. + +Environment: + COPILOT_AGENTS_DIR Override the install target. Defaults to ~/.copilot/agents. + +Examples: + ./scripts/install-copilot-agents.sh + ./scripts/install-copilot-agents.sh --link + COPILOT_AGENTS_DIR="$HOME/.copilot/agents" ./scripts/install-copilot-agents.sh --force +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --copy) + mode="copy" + ;; + --link) + mode="link" + ;; + --force) + force="true" + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac + shift +done + +if [[ ! -d "${source_dir}" ]]; then + echo "Source directory not found: ${source_dir}" >&2 + exit 1 +fi + +mkdir -p "${target_dir}" +shopt -s nullglob +agent_files=("${source_dir}"/*.agent.md) + +if [[ ${#agent_files[@]} -eq 0 ]]; then + echo "No agent files found in ${source_dir}" >&2 + exit 1 +fi + +installed=0 +skipped=0 + +for source_path in "${agent_files[@]}"; do + file_name="$(basename "${source_path}")" + target_path="${target_dir}/${file_name}" + + if [[ -e "${target_path}" || -L "${target_path}" ]]; then + if [[ "${force}" == "true" ]]; then + rm -f "${target_path}" + else + echo "Skipping existing ${target_path} (use --force to replace it)" + skipped=$((skipped + 1)) + continue + fi + fi + + if [[ "${mode}" == "link" ]]; then + ln -s "${source_path}" "${target_path}" + echo "Linked ${file_name} -> ${target_path}" + else + cp "${source_path}" "${target_path}" + echo "Copied ${file_name} -> ${target_path}" + fi + + installed=$((installed + 1)) +done + +echo +echo "Installed ${installed} agent file(s) into ${target_dir}." +if [[ ${skipped} -gt 0 ]]; then + echo "Skipped ${skipped} existing file(s)." +fi From 08f0e33c4b5d47bc257a9c25373b0c94e7504245 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 14 Apr 2026 15:27:27 +0200 Subject: [PATCH 119/199] refactor: replace isort with Ruff for import sorting and linting --- .github/copilot-instructions.md | 5 +- .pre-commit-config.yaml | 9 ---- README.md | 4 +- docs/4_code_quality.md | 4 +- docs/5_pre_commit_hooks.md | 4 +- docs/6_pre_commit_troubleshooting.md | 80 ++++------------------------ pyproject.toml | 3 +- uv.lock | 11 ---- 8 files changed, 20 insertions(+), 100 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index eb2028bb..b611b65f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -152,7 +152,7 @@ uv.lock # Locked dependency versions (auto-generated ### Code Style - **Line length:** 88 characters (Black formatter) -- **Imports:** Sorted by isort (Black profile), auto-fixed by pre-commit +- **Imports:** Sorted by Ruff import-sorting rules, auto-fixed by pre-commit - **Modernization:** pyupgrade auto-converts old syntax (Python 3.12+) - **Docstrings:** Checked by pydocstyle (Google style recommended) @@ -180,7 +180,6 @@ All 16+ hooks run automatically when you commit (if venv is active). |------|---------|------------| | `detect-secrets` | Blocks secrets from being committed | โŒ Manual fix required | | `black` | Code formatting (88-char lines) | โœ… Auto-fixes | -| `isort` | Import sorting (Black profile) | โœ… Auto-fixes | | `pyupgrade` | Modernizes Python syntax | โœ… Auto-fixes | | `ruff` | Comprehensive linting | โœ… Most auto-fixed | | `mypy` | Static type checking | โŒ Manual fix required | @@ -193,7 +192,7 @@ All 16+ hooks run automatically when you commit (if venv is active). ### CI/CD Pipeline (.github/workflows/ci.yml) Runs on every PR and merge to `main`/`staging`: -1. Lint & format check (black, ruff, isort) +1. Lint & format check (black, ruff) 2. Type checking (mypy) 3. Security scanning (bandit, pip-audit, detect-secrets) 4. Unit tests (pytest) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36f3559a..f2412745 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,15 +68,6 @@ repos: name: Type check with mypy args: ["--config-file=pyproject.toml"] - # Import sorting with isort (must run before formatters) - - repo: https://github.com/PyCQA/isort - rev: 8.0.1 - hooks: - - id: isort - name: isort (import sorting) - args: [--profile=black] - types: [python] - # Modernize Python syntax - repo: https://github.com/asottile/pyupgrade rev: v3.21.2 diff --git a/README.md b/README.md index d4834fdf..f8af90f7 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Detailed setup and customization guides are linked in the [๐Ÿ“š Documentation](# ### ๐Ÿ› ๏ธ **Development Tools** - **Code formatting**: Black -- **Linting & import sorting**: Ruff (replaces flake8, isort, pyupgrade) +- **Linting & import sorting**: Ruff (replaces flake8 and pyupgrade) - **Type checking**: mypy & beartype - **Testing**: pytest with coverage - **Pre-commit hooks** for code quality @@ -304,7 +304,7 @@ pytest tests/test_specific.py All development tools are configured in `pyproject.toml`: - **Black**: Code formatting (88 char line length) -- **ruff**: Sorting, linting, formatting (replaces flake8, isort, pyupgrade) +- **ruff**: Sorting, linting, formatting (replaces flake8 and pyupgrade) - **pytest**: Test configuration with coverage - **mypy**: Type checking - **beartype**: Type checking at runtime (more extensive then mypy) diff --git a/docs/4_code_quality.md b/docs/4_code_quality.md index ee04bded..cfe2b1f7 100644 --- a/docs/4_code_quality.md +++ b/docs/4_code_quality.md @@ -58,8 +58,8 @@ Quality tools enforce style, consistency, and early bug detection. Ruff, Black, Ruff provides fast linting and catches many code issues before commit. -- Replaces flake8, isort, and pyupgrade in a single fast tool -- Configured via `pyproject.toml` with enabled rules: E (pycodestyle), F (pyflakes), I (isort), B (bugbear), UP (pyupgrade) +- Replaces flake8 and pyupgrade in a single fast tool +- Configured via `pyproject.toml` with enabled rules: E (pycodestyle), F (pyflakes), I (import sorting), B (bugbear), UP (pyupgrade) - Automatically fixes issues when possible - Target version set to Python 3.11+ with 88-character line length diff --git a/docs/5_pre_commit_hooks.md b/docs/5_pre_commit_hooks.md index 76624452..d55f1738 100644 --- a/docs/5_pre_commit_hooks.md +++ b/docs/5_pre_commit_hooks.md @@ -18,7 +18,7 @@ The following hooks are currently configured: | `detect-secrets` | Detect and prevent committing secrets | | `jupytext` | Sync `.ipynb` and `.py` notebook pairs | | `mypy` | Type check Python code with mypy | -| `ruff` | Lint Python code with Ruff (includes isort) | +| `ruff` | Lint Python code with Ruff (includes import sorting) | The configuration is stored in the `.pre-commit-config.yaml` file at the project root. @@ -81,7 +81,7 @@ The `mypy` hook performs static type checking on Python code to catch type-relat The `ruff` hook provides fast Python linting and includes: - Code quality checks (similar to flake8) -- Import sorting (replacing isort) +- Import sorting (handled by Ruff) - Security vulnerability detection - Performance and bug detection - Automatically fixes issues where possible diff --git a/docs/6_pre_commit_troubleshooting.md b/docs/6_pre_commit_troubleshooting.md index 45054cae..26b6b6b0 100644 --- a/docs/6_pre_commit_troubleshooting.md +++ b/docs/6_pre_commit_troubleshooting.md @@ -78,52 +78,15 @@ pre-commit autoupdate ## Import & Code Formatting Hooks -### isort (Import Sorting) - -**Problem:** isort conflicts with Black formatting - -**Solution:** Ensure Black profile is enabled: -```yaml -- repo: https://github.com/PyCQA/isort - hooks: - - id: isort - args: [--profile=black] # Must specify Black profile! -``` - -**Problem:** isort changes files every run (unstable) - -**Solutions:** -```bash -# Verify pyproject.toml isort config (if any) -cat pyproject.toml | grep -A 10 "\[tool.isort\]" - -# Clear cache -rm -rf .isort.cache - -# Run with verbose output -pre-commit run isort --all-files --verbose - -# Common fix: ensure consistent profile -run: uv run isort --profile=black . -``` - -**Problem:** "Known third party modules not recognized" - -**Solution:** Add to `.isort.cfg` or `pyproject.toml`: -```toml -[tool.isort] -profile = "black" -known_first_party = ["utils", "example"] # Your local packages -``` +Ruff handles import sorting in this repo, so dedicated import-sorting setup is not used. Black remains the canonical formatter, and Ruff provides linting plus import-sorting corrections. ### Black (Code Formatter) -**Problem:** Black conflicts with other formatters (ruff, isort) +**Problem:** Black conflicts with other formatters (ruff) **Solution:** Ensure execution order and compatible configs: -1. isort runs FIRST -2. Then Black -3. Then ruff +1. Black runs FIRST +2. Then ruff **Problem:** "Cannot parse file" or "Unexpected token" @@ -150,20 +113,9 @@ target-version = ["py312"] ### Ruff (Linting & Formatting) -**Problem:** Ruff conflicts with isort or Black - -**Solution:** Disable overlapping rules in `pyproject.toml`: -```toml -[tool.ruff] -line-length = 88 +**Problem:** Ruff conflicts with Black -[tool.ruff.lint] -# Ensure ruff doesn't override isort -ignore = ["I"] # Ignore import sorting (handled by isort) - -[tool.ruff.lint.isort] -profile = "black" -``` +**Solution:** Use consistent line-length and keep formatting rules aligned between Ruff and Black. **Problem:** Ruff fails with "E501: line too long" even after Black @@ -189,7 +141,6 @@ ruff config # Try running in isolation ruff check --fix . black . -isort . ``` ### pyupgrade (Python Modernization) @@ -767,23 +718,17 @@ grep "rev:" .pre-commit-config.yaml ### Import/Format/Lint Cycle (A Modifies, B Undoes, C Redoes) -**Problem:** isort, Black, and ruff keep changing files back and forth. +**Problem:** Black and ruff keep changing files back and forth. -**Solution:** Ensure proper hook order: +**Solution:** Ensure proper hook order and compatible configuration: ```yaml -# 1. Import sorting FIRST -- repo: https://github.com/PyCQA/isort - hooks: - - id: isort - args: [--profile=black] - -# 2. Python modernization +# 1. Python modernization - repo: https://github.com/asottile/pyupgrade hooks: - id: pyupgrade args: [--py312-plus] -# 3. Linting + formatting LAST +# 2. Linting + formatting LAST - repo: https://github.com/astral-sh/ruff-pre-commit hooks: - id: ruff @@ -792,12 +737,9 @@ grep "rev:" .pre-commit-config.yaml And ensure config compatibility: ```toml -[tool.isort] -profile = "black" - [tool.black] line-length = 88 - +``` [tool.ruff] line-length = 88 ignore = ["I"] # Don't let ruff handle imports diff --git a/pyproject.toml b/pyproject.toml index a215b527..a4edc64f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,6 @@ dev = [ "pytest-beartype>=0.2.0,<1.0.0", "pip-audit>=2.10.0,<3.0.0", "rich>=14.2.0,<15.0.0", - "isort>=5.13.0,<9.0.0", "pyupgrade>=3.15.0,<4.0.0", "codespell>=2.2.0,<3.0.0", "bandit>=1.7.0,<2.0.0", @@ -172,7 +171,7 @@ lint.dummy-variable-rgx = "^_|^__|^pytest$" lint.select = [ "E", # pycodestyle "F", # pyflakes - "I", # isort + "I", # import sorting "B", # bugbear "UP", # pyupgrade ] diff --git a/uv.lock b/uv.lock index efc202a8..1cf17a4c 100644 --- a/uv.lock +++ b/uv.lock @@ -615,15 +615,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] -[[package]] -name = "isort" -version = "8.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, -] - [[package]] name = "itsdangerous" version = "2.2.0" @@ -1391,7 +1382,6 @@ dev = [ { name = "detect-secrets" }, { name = "genbadge", extra = ["coverage"] }, { name = "ipykernel" }, - { name = "isort" }, { name = "mypy" }, { name = "nbstripout" }, { name = "pillow" }, @@ -1425,7 +1415,6 @@ dev = [ { name = "detect-secrets", specifier = ">=1.5.0,<2.0.0" }, { name = "genbadge", extras = ["coverage"], specifier = ">=1.1.0,<2.0.0" }, { name = "ipykernel", specifier = ">=6.29.0,<7.0.0" }, - { name = "isort", specifier = ">=5.13.0,<9.0.0" }, { name = "mypy", specifier = ">=1.19.0,<2.0.0" }, { name = "nbstripout", specifier = ">=0.8.0,<1.0.0" }, { name = "pillow", specifier = "~=12.2" }, From ae5557b72bc5a065dd5de267145bd832f56221c6 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 14 Apr 2026 15:36:12 +0200 Subject: [PATCH 120/199] feat: add pre-commit compliance guidelines to Coder and Orchestrator agents --- .copilot/agents/coder.agent.md | 6 ++++++ .copilot/agents/orchestrator.agent.md | 3 +++ 2 files changed, 9 insertions(+) diff --git a/.copilot/agents/coder.agent.md b/.copilot/agents/coder.agent.md index a5dddb8f..3673cc16 100644 --- a/.copilot/agents/coder.agent.md +++ b/.copilot/agents/coder.agent.md @@ -49,3 +49,9 @@ These coding principles are mandatory: 9. Quality - Favor deterministic, testable behavior. - Keep tests simple and focused on verifying observable behavior. + +10. Pre-commit Compliance +- Use the repository's pre-commit configuration as the gate for completed work. +- After making changes, verify the modified files pass the repo's pre-commit checks before finalizing. +- If code does not comply, fix the issues until the hooks succeed. +- Prefer running the equivalent of `pre-commit run --files ` or `pre-commit run --all-files` where available. diff --git a/.copilot/agents/orchestrator.agent.md b/.copilot/agents/orchestrator.agent.md index c679a929..16228535 100644 --- a/.copilot/agents/orchestrator.agent.md +++ b/.copilot/agents/orchestrator.agent.md @@ -64,6 +64,9 @@ For each phase: After all phases complete, verify the work hangs together and report results. +- Confirm the final changes comply with the repository's pre-commit checks. +- If any pre-commit hook would fail, do not finalize the task; instead create a follow-up fix task for Coder and rerun verification. + ## Parallelization Rules **RUN IN PARALLEL when:** From f7e33f8be5ca86df9ded9822a8ee50da327a7197 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 15 Apr 2026 13:29:12 +0200 Subject: [PATCH 121/199] refactor: update VSCode settings for mypy type checker configuration and clean up pyproject.toml --- .vscode/settings.json | 11 ++++++----- pyproject.toml | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b6c5d03d..e6b2045f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,12 @@ { - "python.analysis.typeCheckingMode": "standard", - "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", - "python.linting.enabled": true, - "python.linting.mypyArgs": [ + "mypy-type-checker.args": [ "--config-file=pyproject.toml" ], - "python.linting.mypyEnabled": true, + "mypy-type-checker.cwd": "${workspaceFolder}", + "mypy-type-checker.importStrategy": "fromEnvironment", + "mypy-type-checker.reportingScope": "workspace", + "python.analysis.typeCheckingMode": "standard", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "python.testing.pytestArgs": [ "." ], diff --git a/pyproject.toml b/pyproject.toml index a4edc64f..f0313703 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ extra = [ # "zero-friction @ git+https://github.com/TECHLETES/zero_friction.git@main" ] - [tool.setuptools.packages.find] where = ["."] include = ["utils*", "example*"] @@ -149,7 +148,18 @@ warn_unused_configs = true disallow_untyped_defs = true disallow_incomplete_defs = true -exclude = ["migrations", "notebooks", "tests"] +# Specify which folders/files to type check (adjust as needed) +files = ["example"] + +exclude = ''' +(?x)( + ^migrations/ + | ^notebooks/ + | ^tests/ + | ^\.venv/ + | ^venv/ +) +''' plugins = [] # Loosen rules for test and script files From 7c5aada6ca5bc03163072f9af98d8f045c12facf Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Thu, 16 Apr 2026 10:42:05 +0200 Subject: [PATCH 122/199] chore: update dependabot configuration for daily dependency checks and streamline package ecosystems --- .github/dependabot.yml | 44 +++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 95af84f1..1ffbfd4e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,29 +1,33 @@ version: 2 -multi-ecosystem-groups: - all-updates: +updates: + # Python dependencies (uv project) + - package-ecosystem: "uv" + directory: "/" schedule: - interval: "weekly" - day: "monday" - time: "06:00" - timezone: "Europe/Amsterdam" - labels: - - "dependencies" - - "dependabot" + interval: "daily" + open-pull-requests-limit: 0 + + # Web dependencies via package.json (Bun-compatible through npm ecosystem) + - package-ecosystem: "npm" + directory: "/web" + schedule: + interval: "daily" + open-pull-requests-limit: 0 commit-message: prefix: "deps" - include: "scope" - target-branch: "main" + versioning-strategy: increase-if-necessary -updates: - - package-ecosystem: "github-actions" + # Dockerfile base images + - package-ecosystem: "docker" directory: "/" - multi-ecosystem-group: "all-updates" - patterns: - - "*" + schedule: + interval: "daily" + open-pull-requests-limit: 0 - - package-ecosystem: "uv" + # GitHub Actions + - package-ecosystem: "github-actions" directory: "/" - multi-ecosystem-group: "all-updates" - patterns: - - "*" + schedule: + interval: "daily" + open-pull-requests-limit: 0 From ff4de40d76e0346664947acb791f116f56334d49 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Thu, 16 Apr 2026 13:56:17 +0200 Subject: [PATCH 123/199] refactor: remove unused lock file exports and update mypy configuration in pyproject.toml --- .envrc | 3 --- pyproject.toml | 9 +-------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/.envrc b/.envrc index eea419c0..1ee6f0cb 100644 --- a/.envrc +++ b/.envrc @@ -7,9 +7,6 @@ fi export PYTHONPATH=. export VENV_DIR=".venv" -export PROD_LOCK="requirements.txt" -export DEV_LOCK="requirements-dev.txt" -export SLIM_LOCK="requirements-slim.txt" export PYPROJECT="pyproject.toml" # Set a virtualenv automatically if venv exists diff --git a/pyproject.toml b/pyproject.toml index f0313703..cde02faa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,13 +162,6 @@ exclude = ''' ''' plugins = [] -# Loosen rules for test and script files -[[tool.mypy.overrides]] -module = ["tests.*", "scripts.*"] -disallow_untyped_defs = false -disallow_incomplete_defs = false -warn_return_any = false - [tool.ruff] target-version = "py312" line-length = 88 @@ -193,7 +186,7 @@ lint.ignore = [ ] [tool.bandit] -exclude_dirs = ["tests", "venv"] +exclude_dirs = ["tests", ".venv"] skips = ["B101", "B601"] [tool.pip-audit] From fc3f3dd50df5a6952fec5f0856726e29af30e295 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Thu, 16 Apr 2026 14:49:13 +0200 Subject: [PATCH 124/199] refactor: update pytest hook configuration to specify Python file types Co-authored-by: Copilot --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2412745..8943be2b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -113,5 +113,5 @@ repos: entry: uv run pytest --cov --cov-fail-under=80 --beartype-packages=utils,example language: system pass_filenames: false - always_run: true + types: [python] stages: [pre-commit] From a457f37fff87271fda4b360dd666e8d90cc0c901 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 17 Apr 2026 14:38:16 +0200 Subject: [PATCH 125/199] feat: enhance installation script to support skills installation and add pip-audit helper script Co-authored-by: Copilot --- .../SKILL.md | 359 ++++++++++++++++++ .../run-pip-audit.sh | 27 ++ scripts/install-copilot-agents.sh | 184 ++++++--- 3 files changed, 527 insertions(+), 43 deletions(-) create mode 100644 .copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md create mode 100644 .copilot/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh diff --git a/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md b/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md new file mode 100644 index 00000000..0eaad66d --- /dev/null +++ b/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md @@ -0,0 +1,359 @@ +--- +name: update-dependencies-fix-vulnerabilities +description: Systematically scan for and fix security vulnerabilities in Python (pip audit) and JavaScript/bun (bun audit) dependencies. Use this skill when the user asks to fix security vulnerabilities, update dependencies, patch CVEs, or audit packages for security issues. +license: Complete terms in LICENSE.txt +--- + +# Dependency Vulnerability Remediation Skill + +This skill guides systematic identification and resolution of security vulnerabilities in Python and JavaScript/TypeScript dependencies. It handles scanning, prioritization, conflict resolution, and validation. + +## When to Use This Skill + +Use this skill when: +- Running security audits to identify vulnerabilities +- Fixing CVEs and security patches +- Updating dependencies to resolve known issues +- Cleaning up ignored/suppressed vulnerabilities that now have fixes +- Validating that applications remain functional after dependency updates + +## Workflow Overview + +The skill uses a three-phase approach: +1. **Scan Phase**: Identify all vulnerabilities +2. **Resolution Phase**: Update versions and resolve conflicts +3. **Validation Phase**: Ensure application stability + +--- + +## Phase 1: Python Dependency Scan + +### Step 1a: Run pip-audit Scanner + +Use the `run-pip-audit.sh` helper script to scan for Python vulnerabilities. This script automatically applies CVE exclusions from `pyproject.toml` `[tool.pip-audit]`. + +```bash +./scripts/hooks/run-pip-audit.sh --progress-spinner off --desc +``` + +**Capture Output**: Document all reported CVEs with: +- CVE ID (e.g., CVE-2024-XXXXX) +- Package name +- Affected versions +- Fixed version (if available) +- Severity level + +**Decision Point**: Are there vulnerabilities? +- **No vulnerabilities**: Proceed to Phase 2 (JavaScript/bun, if applicable) +- **Vulnerabilities found**: Continue to Step 1b + +### Step 1b: Categorize Vulnerabilities + +For each CVE, determine: + +1. **Is it in ignored list?** Check `pyproject.toml` `[tool.pip-audit]` `ignore` array + - If YES: Check if a fixed version now exists for this package + - If NO: Proceed to Step 1c + +2. **Severity & Impact**: + - **CRITICAL**: Remote code execution, authentication bypass + - **HIGH**: Data breach potential, privilege escalation + - **MEDIUM**: Denial of service, information disclosure + - **LOW**: Edge cases, minor impacts + +3. **Fixability**: + - **Direct fix**: Update package to fixed version + - **Requires conflict resolution**: Multiple dependencies on conflicting versions + - **No fix available**: Package abandoned or maintainer not responsive + - **Requires major version bump**: Breaking changes + +### Step 1c: Update Python Dependencies + +For each vulnerability (prioritize CRITICAL first): + +1. **Identify the affected package** from the audit output +2. **Check the current version** in `pyproject.toml` `[dependencies]` or `[dependency-groups.dev]` +3. **Add or update the package** to a version that fixes the CVE: + ```toml + [dependencies] + # Old: requests = ">=2.28.0,<3.0.0" # Vulnerable to CVE-2024-XXXXX in 2.28.0-2.30.x + requests = ">=2.31.0,<3.0.0" # Fixed in 2.31.0+ + ``` + +4. **Use semantic versioning**: `package>=MAJOR.MINOR.Z,=2.0,<3.0 + โ””โ”€ package-b 2.0 requires package-c >=1.5,<2.0 + ``` + +2. **Resolution strategies** (in order of preference): + - **Update one conflicting package** to a newer version that relaxes constraints + - **Replace package** with a maintained alternative + - **Accept a lower constraint** (accept a not-fully-fixed CVE if severity permits) + - **Open an issue** with the maintainer if a fix is needed but unavailable + +3. **Iterate**: + - Make one change at a time + - Run `uv lock` to validate + - If still failing, make the next change + - Repeat until `uv lock` succeeds + +4. **Document decisions**: Add comments in `pyproject.toml` explaining complex choices: + ```toml + # Conflict: package-a requires c>=2.0 but package-b needs c<2.0 + # Resolution: Updated package-b to 3.0 which supports c>=2.0 + package-b = ">=3.0,<4.0" + ``` + +--- + +## Phase 1 Validation: Python Tests + +Once dependencies are locked and synced: + +```bash +uv sync +uv run pytest +``` + +**Decision Point**: Do all tests pass? +- **YES**: Python vulnerabilities are resolved โœ“ +- **NO**: There is a compatibility issue + - Review test output carefully + - Check if the new package version has breaking changes + - Consider reverting to a previous compatible version + - Or update code to match new API + +--- + +## Phase 2: JavaScript/Bun Dependency Scan (if applicable) + +Only proceed if the project has JavaScript/TypeScript dependencies (check for `package.json` and `bun.lock`). + +### Step 2a: Check for bun.lock and package.json + +```bash +ls -la package.json bun.lock +``` + +**Decision Point**: Do both files exist? +- **NO**: Skip Phase 2, proceed to completion +- **YES**: Continue to Step 2b + +### Step 2b: Run bun audit + +```bash +bun audit +``` + +**Capture Output**: Document all vulnerabilities with: +- Package name +- Current version +- Vulnerable range +- Fixed version +- Severity level + +**Decision Point**: Are there vulnerabilities? +- **No vulnerabilities**: Proceed to completion +- **Vulnerabilities found**: Continue to Step 2c + +### Step 2c: Update Bun Packages (Non-Breaking) + +Run automatic non-breaking updates: + +```bash +bun update +``` + +This updates all packages to their latest non-breaking versions (respects semver constraints in `package.json`). + +### Step 2d: Re-audit After Updates + +```bash +bun audit +``` + +**Decision Point**: Are vulnerabilities resolved? +- **YES**: Continue to Step 2e +- **NO**: Continue to Step 2f (Manual Resolution) + +### Step 2e: Manual Vulnerability Resolution (if needed) + +For remaining vulnerabilities: + +1. **Identify the affected package** and required fixed version +2. **Check package.json constraint**: Can it be updated? + ```json + { + "dependencies": { + "vulnerable-package": "^1.2.0" // Requires ^1.0 + } + } + ``` + +3. **Update the version constraint** to allow the fixed version: + ```json + { + "dependencies": { + "vulnerable-package": "^1.5.0" // Now allows 1.5.0 fix + } + } + ``` + +4. **Install updates**: + ```bash + bun install + ``` + +5. **Re-audit**: + ```bash + bun audit + ``` + +6. **Iterate** until all critical/high vulnerabilities are resolved + +--- + +## Phase 2 Validation: Frontend Tests + +Once bun.lock is updated and all packages installed: + +1. **Run dev server** (verify no startup errors): + ```bash + bun run dev + ``` + - Allow 5-10 seconds for full startup + - Check console for errors + - Ctrl+C to stop + +2. **Run tests** (if frontend tests exist): + ```bash + bun run test + ``` + - Or: `bun run test:ui` if available + - All tests must pass + +**Decision Point**: Do dev server and tests work? +- **YES**: JavaScript vulnerabilities are resolved โœ“ +- **NO**: There is a compatibility issue + - Review error messages + - Check for breaking changes in updated packages + - Consider a more conservative version constraint + - Review package changelogs for migration guides + +--- + +## Completion Checklist + +โœ“ All Python CVEs (Critical/High/Medium) are fixed or justified (Low severity ignored) +โœ“ `uv lock` generates successfully with no version conflicts +โœ“ `uv sync` completes without errors +โœ“ `pytest` passes all tests +โœ“ Ignored CVEs in `[tool.pip-audit]` are reviewed and cleaned up if fixed +โœ“ JavaScript/bun vulnerabilities are scanned and resolved (if applicable) +โœ“ `bun audit` shows no critical/high vulnerabilities +โœ“ `bun run dev` starts without errors +โœ“ Frontend tests pass (if applicable) +โœ“ Commit message documents CVEs fixed and any version constraint changes + +--- + +## Decision Tree: When to Stop Iterating + +**For Python (pip audit):** +- Stop when: All CRITICAL and HIGH CVEs are fixed, and all tests pass +- Exception: Document and justify any LOW severity CVEs left unfixed +- Never commit unresolved CRITICAL vulnerabilities + +**For JavaScript/Bun:** +- Stop when: All CRITICAL and HIGH vulnerabilities are fixed +- Exception: Can accept LOW severity if no fix is available +- Never commit unresolved CRITICAL vulnerabilities + +**Version Conflicts:** +- Iterate max 3-5 times; if no solution, escalate to maintainers +- Consider alternative packages if maintainer is unresponsive + +--- + +## Helpful Context: Files & Commands + +| Task | Command/File | +|------|---------| +| Scan Python | `./scripts/hooks/run-pip-audit.sh --progress-spinner off --desc` | +| View ignored CVEs | `pyproject.toml` โ†’ `[tool.pip-audit]` `ignore` | +| Lock Python deps | `uv lock` | +| Install Python deps | `uv sync` | +| Run Python tests | `uv run pytest` | +| Scan JavaScript | `bun audit` | +| Update JS deps | `bun update` โ†’ `bun install` | +| Run frontend | `bun run dev` | +| Run JS tests | `bun run test` | + +--- + +## Anti-Patterns to Avoid + +โŒ **Don't** update everything at once; update prioritized vulnerabilities +โŒ **Don't** skip tests; always validate after dependency updates +โŒ **Don't** ignore CRITICAL vulnerabilities; fix them or mitigate with security controls +โŒ **Don't** commit ignores for known fixable CVEs; update to fixed versions +โŒ **Don't** manually edit `uv.lock`; let `uv lock` generate it +โŒ **Don't** use `pip install` directly; always edit `pyproject.toml` +โŒ **Don't** accept all major version bumps; use semver constraints to block breaking changes +โŒ **Don't** skip re-auditing; verify fixes worked + +--- + +## Success Criteria + +A successful vulnerability remediation: +1. **All CRITICAL vulnerabilities are fixed** +2. **All HIGH vulnerabilities are fixed** +3. **All code tests pass** (Python + JavaScript, if applicable) +4. **All applications start without errors** +5. **Dependency versions are locked in version control** (`uv.lock`, `bun.lock`) +6. **Changes are committed with clear messaging** about fixed CVEs + +--- + +## Example Prompts to Use This Skill + +- "Fix all vulnerabilities in the Python dependencies" +- "Run pip audit and npm audit, then update any critical CVEs" +- "Clean up old ignored CVEs in pip-audit and update them if fixes exist" +- "Update all dependencies to latest non-breaking versions" +- "Resolve the version conflict between package-a and package-b" +- "Validate that the app still works after the latest security updates" diff --git a/.copilot/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh b/.copilot/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh new file mode 100644 index 00000000..0202b037 --- /dev/null +++ b/.copilot/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Helper script to build pip-audit ignore arguments from pyproject.toml [tool.pip-audit] +# This ensures both pre-commit and CI use the same CVE exclusion list + +# Extract ignore list from pyproject.toml using Python +IGNORE_ARGS=$(python3 << 'EOF' +import tomllib +import sys + +try: + with open("pyproject.toml", "rb") as f: + config = tomllib.load(f) + + # Get the ignore list from [tool.pip-audit] + ignore_list = config.get("tool", {}).get("pip-audit", {}).get("ignore", []) + + # Build the arguments + args = " ".join([f"--ignore-vuln {cve}" for cve in ignore_list]) + print(args) +except Exception as e: + print(f"Error reading pyproject.toml: {e}", file=sys.stderr) + sys.exit(1) +EOF +) + +# Run pip-audit with the exclusions from pyproject.toml +pip-audit $IGNORE_ARGS "$@" diff --git a/scripts/install-copilot-agents.sh b/scripts/install-copilot-agents.sh index 02b0a040..b067b975 100755 --- a/scripts/install-copilot-agents.sh +++ b/scripts/install-copilot-agents.sh @@ -2,30 +2,34 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -source_dir="${repo_root}/.copilot/agents" -target_dir="${COPILOT_AGENTS_DIR:-${HOME}/.copilot/agents}" mode="copy" force="false" +install_agents="true" +install_skills="true" usage() { cat <<'EOF' -Install the shared Copilot agent definitions from this repository. +Install Copilot agent definitions and skills from this repository. Usage: - ./scripts/install-copilot-agents.sh [--copy|--link] [--force] + ./scripts/install-copilot-agents.sh [--copy|--link] [--force] [--agents-only|--skills-only] Options: - --copy Copy the agent files into the target directory (default). - --link Symlink the agent files into the target directory. - --force Replace existing files with the repository versions. - --help Show this help message. + --copy Copy the files into the target directory (default). + --link Symlink the files into the target directory. + --force Replace existing files with the repository versions. + --agents-only Install only agent definitions (skip skills). + --skills-only Install only skills (skip agents). + --help Show this help message. Environment: - COPILOT_AGENTS_DIR Override the install target. Defaults to ~/.copilot/agents. + COPILOT_AGENTS_DIR Override agents install target. Defaults to ~/.copilot/agents. + COPILOT_SKILLS_DIR Override skills install target. Defaults to ~/.copilot/skills. Examples: ./scripts/install-copilot-agents.sh ./scripts/install-copilot-agents.sh --link + ./scripts/install-copilot-agents.sh --skills-only COPILOT_AGENTS_DIR="$HOME/.copilot/agents" ./scripts/install-copilot-agents.sh --force EOF } @@ -41,6 +45,12 @@ while [[ $# -gt 0 ]]; do --force) force="true" ;; + --agents-only) + install_skills="false" + ;; + --skills-only) + install_agents="false" + ;; --help|-h) usage exit 0 @@ -54,50 +64,138 @@ while [[ $# -gt 0 ]]; do shift done -if [[ ! -d "${source_dir}" ]]; then - echo "Source directory not found: ${source_dir}" >&2 - exit 1 -fi +# Function to install files from source to target directory +install_copilot_component() { + local component_name="$1" + local source_dir="$2" + local target_dir="$3" + local file_pattern="$4" -mkdir -p "${target_dir}" -shopt -s nullglob -agent_files=("${source_dir}"/*.agent.md) + if [[ ! -d "${source_dir}" ]]; then + echo "Skipping ${component_name}: source directory not found (${source_dir})" + return 1 + fi -if [[ ${#agent_files[@]} -eq 0 ]]; then - echo "No agent files found in ${source_dir}" >&2 - exit 1 -fi + mkdir -p "${target_dir}" + shopt -s nullglob + local source_files=("${source_dir}"/${file_pattern}) -installed=0 -skipped=0 + if [[ ${#source_files[@]} -eq 0 ]]; then + echo "No ${component_name} files found in ${source_dir}" + return 1 + fi -for source_path in "${agent_files[@]}"; do - file_name="$(basename "${source_path}")" - target_path="${target_dir}/${file_name}" + local installed=0 + local skipped=0 + local total=${#source_files[@]} + + for source_path in "${source_files[@]}"; do + local file_name="$(basename "${source_path}")" + local target_path="${target_dir}/${file_name}" + + if [[ -e "${target_path}" || -L "${target_path}" ]]; then + if [[ "${force}" == "true" ]]; then + rm -f "${target_path}" + else + echo " Skipping existing ${file_name} (use --force to replace)" + skipped=$((skipped + 1)) + continue + fi + fi - if [[ -e "${target_path}" || -L "${target_path}" ]]; then - if [[ "${force}" == "true" ]]; then - rm -f "${target_path}" + if [[ "${mode}" == "link" ]]; then + ln -s "${source_path}" "${target_path}" + echo " Linked ${file_name}" else - echo "Skipping existing ${target_path} (use --force to replace it)" - skipped=$((skipped + 1)) - continue + cp -r "${source_path}" "${target_path}" + echo " Copied ${file_name}" fi + + installed=$((installed + 1)) + done + + echo "${component_name}: Installed ${installed}/${total} file(s) to ${target_dir}" + if [[ ${skipped} -gt 0 ]]; then + echo " (${skipped} existing file(s) skipped)" fi - if [[ "${mode}" == "link" ]]; then - ln -s "${source_path}" "${target_path}" - echo "Linked ${file_name} -> ${target_path}" - else - cp "${source_path}" "${target_path}" - echo "Copied ${file_name} -> ${target_path}" + return 0 +} + +# Install agents +if [[ "${install_agents}" == "true" ]]; then + echo "Installing Copilot Agents..." + agents_source="${repo_root}/.copilot/agents" + agents_target="${COPILOT_AGENTS_DIR:-${HOME}/.copilot/agents}" + install_copilot_component "Agents" "${agents_source}" "${agents_target}" "*.agent.md" +fi + +# Function to install skills (subdirectories with SKILL.md) +install_skills_function() { + local skills_source="$1" + local skills_target="$2" + local mode_arg="$3" + local force_arg="$4" + + if [[ ! -d "${skills_source}" ]]; then + echo "Skipping skills: source directory not found (${skills_source})" + return 1 fi - installed=$((installed + 1)) -done + mkdir -p "${skills_target}" + shopt -s nullglob + local skill_dirs=("${skills_source}"/*/SKILL.md) -echo -echo "Installed ${installed} agent file(s) into ${target_dir}." -if [[ ${skipped} -gt 0 ]]; then - echo "Skipped ${skipped} existing file(s)." + if [[ ${#skill_dirs[@]} -eq 0 ]]; then + echo "No skill definitions found in ${skills_source}" + return 1 + fi + + local installed=0 + local skipped=0 + + # Process each skill (subdirectory with SKILL.md) + for skill_skill_md in "${skill_dirs[@]}"; do + local skill_dir="$(dirname "${skill_skill_md}")" + local skill_name="$(basename "${skill_dir}")" + local target_skill_dir="${skills_target}/${skill_name}" + + if [[ -e "${target_skill_dir}" || -L "${target_skill_dir}" ]]; then + if [[ "${force_arg}" == "true" ]]; then + rm -rf "${target_skill_dir}" + else + echo " Skipping existing ${skill_name}/ (use --force to replace)" + skipped=$((skipped + 1)) + continue + fi + fi + + if [[ "${mode_arg}" == "link" ]]; then + ln -s "${skill_dir}" "${target_skill_dir}" + echo " Linked ${skill_name}/" + else + cp -r "${skill_dir}" "${target_skill_dir}" + echo " Copied ${skill_name}/" + fi + + installed=$((installed + 1)) + done + + echo "Skills: Installed ${installed} skill(s) to ${skills_target}" + if [[ ${skipped} -gt 0 ]]; then + echo " (${skipped} existing skill(s) skipped)" + fi + + return 0 +} + +# Install skills (including subdirectories) +if [[ "${install_skills}" == "true" ]]; then + echo "Installing Copilot Skills..." + skills_source="${repo_root}/.copilot/skills" + skills_target="${COPILOT_SKILLS_DIR:-${HOME}/.copilot/skills}" + install_skills_function "${skills_source}" "${skills_target}" "${mode}" "${force}" fi + +echo +echo "โœ“ Installation complete!" From 81c2637ab3e74660ef73d0ebf814d646739ddc50 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 17 Apr 2026 14:41:47 +0200 Subject: [PATCH 126/199] refactor: simplify pip-audit command by removing unnecessary flags --- .../skills/update-dependencies-fix-vulnerabilities/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md b/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md index 0eaad66d..74464a54 100644 --- a/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md +++ b/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md @@ -33,7 +33,7 @@ The skill uses a three-phase approach: Use the `run-pip-audit.sh` helper script to scan for Python vulnerabilities. This script automatically applies CVE exclusions from `pyproject.toml` `[tool.pip-audit]`. ```bash -./scripts/hooks/run-pip-audit.sh --progress-spinner off --desc +./scripts/hooks/run-pip-audit.sh ``` **Capture Output**: Document all reported CVEs with: @@ -312,7 +312,7 @@ Once bun.lock is updated and all packages installed: | Task | Command/File | |------|---------| -| Scan Python | `./scripts/hooks/run-pip-audit.sh --progress-spinner off --desc` | +| Scan Python | `./scripts/hooks/run-pip-audit.sh` | | View ignored CVEs | `pyproject.toml` โ†’ `[tool.pip-audit]` `ignore` | | Lock Python deps | `uv lock` | | Install Python deps | `uv sync` | From 1be106f70968210008dd947a9880312548c1c324 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 17 Apr 2026 14:55:55 +0200 Subject: [PATCH 127/199] feat: enhance dependency vulnerability remediation skill with detailed workflow and commit guidelines --- .../SKILL.md | 316 ++++++++++++++++-- 1 file changed, 292 insertions(+), 24 deletions(-) diff --git a/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md b/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md index 74464a54..96492f84 100644 --- a/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md +++ b/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md @@ -19,10 +19,64 @@ Use this skill when: ## Workflow Overview -The skill uses a three-phase approach: -1. **Scan Phase**: Identify all vulnerabilities -2. **Resolution Phase**: Update versions and resolve conflicts -3. **Validation Phase**: Ensure application stability +The skill uses a four-phase approach: +1. **Preparation Phase**: Set up Git branch for changes +2. **Scan Phase**: Identify all vulnerabilities +3. **Resolution Phase**: Update versions and resolve conflicts +4. **Validation Phase**: Ensure application stability and create PR + +--- + +## Phase 0: Preparation - Git Branch Setup + +### Step 0a: Determine Base Branch + +Identify the appropriate base branch for the security updates: + +```bash +# Check if staging branch exists +git branch -r | grep -E 'origin/staging|staging' + +# If staging exists, use it; otherwise use main or master +if git show-ref --quiet refs/heads/staging; then + BASE_BRANCH="staging" +elif git show-ref --quiet refs/heads/main; then + BASE_BRANCH="main" +else + BASE_BRANCH="master" +fi +``` + +**Decision Point**: Which branch will be the base? +- **staging**: Recommended for staged deployments +- **main/master**: Use if staging doesn't exist +- Document the chosen base branch + +### Step 0b: Create Feature Branch + +Create a new branch with a descriptive name for security updates: + +```bash +# Switch to base branch and pull latest +git checkout $BASE_BRANCH +git pull origin $BASE_BRANCH + +# Create feature branch +git checkout -b security-updates-dependencies +``` + +**Branch naming convention**: `security-updates-dependencies` or `security/dependencies-update-YYYY-MM-DD` + +### Step 0c: Verify Branch Creation + +```bash +git branch --show-current +# Should output: security-updates-dependencies +``` + +**Decision Point**: Is the branch created and checked out? +- **YES**: Continue to Phase 1 (Scan) +- **NO**: Review Git errors and resolve before proceeding --- @@ -137,6 +191,15 @@ If `uv lock` fails with version conflicts: package-b = ">=3.0,<4.0" ``` +### Step 1g: Commit Python Changes + +```bash +git add pyproject.toml uv.lock +git commit -m "security: update Python dependencies - fix CVEs in pypdf, langsmith, langchain-text-splitters" +``` + +Use commit message format: `security: ` + --- ## Phase 1 Validation: Python Tests @@ -273,38 +336,215 @@ Once bun.lock is updated and all packages installed: - Consider a more conservative version constraint - Review package changelogs for migration guides +### Step 2g: Commit JavaScript Changes + +```bash +git add package.json bun.lock +git commit -m "security: update JavaScript dependencies - fix CVEs in frontend packages" +``` + +Use commit message format: `security: ` + +--- + +## Phase 3: Pull Request and Summary + +### Step 3a: Push Branch to Remote + +```bash +git push origin security-updates-dependencies +``` + +### Step 3b: Create Pull Request with Detailed Summary + +Create a PR from `security-updates-dependencies` to the base branch (staging/main/master) with a comprehensive description covering: + +#### PR Title Format +``` +security: fix vulnerability updates in Python and JavaScript dependencies +``` + +#### PR Description Template + +```markdown +## Security Update Summary + +### Python Vulnerabilities Fixed +- **pypdf**: Updated from 6.10.0 โ†’ 6.10.2 + - Fixed: GHSA-jj6c-8h6c-hppx (DoS via malformed PDFs) + - Fixed: GHSA-4pxv-j86v-mhcw (DoS via incremental mode) + - Fixed: GHSA-7gw9-cf7v-778f (Memory exhaustion via FlateDecode) + - Fixed: GHSA-x284-j5p8-9c5p (Memory exhaustion via image FlateDecode) + +- **langsmith**: Updated from 0.7.30 โ†’ 0.7.31 + - Fixed: GHSA-rr7j-v2q5-chgv (Output redaction bypass in streaming) + +- **langchain-text-splitters**: Updated from 1.1.1 โ†’ 1.1.2 + - Fixed: GHSA-fv5p-p927-qmxr (SSRF via redirect validation bypass) + +- **langchain-core**: Updated from 1.2.28 โ†’ 1.2.31 (transitive dependency) + +### Python Vulnerabilities Not Fixed / Ignored + +- **CVE-2026-1703** (pip): Ignored - Low impact path traversal in wheel extraction + - Status: Pre-existing ignore in pyproject.toml + - Reason: Applies only to pip's internal wheel extraction + +- **CVE-2026-2473** (Google Cloud Platform): Ignored - Service-specific issue + - Status: Pre-existing ignore in pyproject.toml + - Reason: Does not affect RAGFlow core functionality + +- **CVE-2025-69872** (diskcache): Ignored - Maintainer inactive + - Status: Pre-existing ignore in pyproject.toml + - Reason: No fix available; low security impact + +### JavaScript Vulnerabilities + +**Status**: 19+ vulnerabilities identified in `bun audit` + +**CRITICAL/HIGH vulnerabilities analyzed**: +- **immer** (^11.1.4): Already at safe version (above vulnerable <9.0.6 range) +- **dompurify** (^3.4.0): Already at safe version (above vulnerable <3.3.2 range) +- **Other vulnerabilities**: Primarily in transitive build/dev dependencies (webpack, eslint, jest, UMI framework) + +**Why some remain unfixed**: +1. **Transitive dev dependencies**: Build-time only, not runtime exposure + - webpack, eslint, jest, typescript build tools + - Require upstream updates from UMI framework maintainers + +2. **No newer versions available**: + - xlsx: Latest 0.18.5 (no 0.19.3+ version exists on npm) + +3. **Pending upstream fixes**: + - path-to-regexp, minimatch, lodash.template, underscore + - Used by framework; awaiting UMI / design system updates + +**Recommendation**: Monitor bun audit output in CI/CD. Most vulnerabilities will resolve when UMI 5.x or Ant Design 6.5+ updates are adopted. + +### Validation Results + +โœ… **Python**: +- pip-audit clean: "No known vulnerabilities found" +- All imports successful +- No breaking changes detected + +โœ… **JavaScript**: +- bun install successful +- Dev dependencies installed +- Build pipeline intact + +### Testing + +- [x] Python imports validated +- [x] uv lock/sync successful +- [x] bun install successful +- [x] No build errors observed + +### Files Changed +- `pyproject.toml` - Updated pypdf, added langchain-text-splitters and langsmith +- `uv.lock` - Regenerated with fixed dependency versions +- `web/package.json` - (if any JavaScript updates applied) +- `web/bun.lock` - (if any JavaScript updates applied) + +### Related Issues +- Security scanning / dependency audit +- CVE remediation + +### Checklist +- [x] All CRITICAL Python CVEs fixed +- [x] All HIGH Python CVEs fixed +- [x] No new CRITICAL JavaScript vulnerabilities introduced +- [x] Tests pass (Python) +- [x] Build pipeline verified (JavaScript) +- [x] Lock files updated and committed +- [x] Comprehensive PR description provided +``` + +### Step 3c: Create Pull Request + +Use available agent tools or the GitHub CLI to create the PR. Avoid using manual web interfaces. + +**Method 1: Using Agent Tools (Preferred)** +If the `create-pull-request` skill or `github-pull-request_create_pull_request` tool is available, use it directly with the summary generated in Step 3b. + +**Method 2: Using GitHub CLI** +If agent tools are unavailable but the `gh` CLI is installed: +```bash +# Save summary to a temp file +echo "SUMMARY_FROM_STEP_3B" > /tmp/pr-body.md + +# Create PR +gh pr create \ + --title "security: fix vulnerability updates in Python and JavaScript dependencies" \ + --body-file /tmp/pr-body.md \ + --base $BASE_BRANCH \ + --head security-updates-dependencies \ + --label "security,dependencies" +``` + --- ## Completion Checklist +โœ“ Base branch identified (staging/main/master) +โœ“ Feature branch `security-updates-dependencies` created and checked out โœ“ All Python CVEs (Critical/High/Medium) are fixed or justified (Low severity ignored) โœ“ `uv lock` generates successfully with no version conflicts โœ“ `uv sync` completes without errors โœ“ `pytest` passes all tests -โœ“ Ignored CVEs in `[tool.pip-audit]` are reviewed and cleaned up if fixed -โœ“ JavaScript/bun vulnerabilities are scanned and resolved (if applicable) -โœ“ `bun audit` shows no critical/high vulnerabilities +โœ“ Ignored CVEs in `[tool.pip-audit]` are reviewed and documented in PR +โœ“ JavaScript/bun vulnerabilities are scanned and analyzed +โœ“ All CRITICAL/HIGH JavaScript vulnerabilities checked (resolved or documented) โœ“ `bun run dev` starts without errors โœ“ Frontend tests pass (if applicable) -โœ“ Commit message documents CVEs fixed and any version constraint changes +โœ“ Python changes committed to branch +โœ“ JavaScript changes committed to branch (if applicable) +โœ“ Branch pushed to remote (`git push origin security-updates-dependencies`) +โœ“ Pull Request created with: + - โœ“ Detailed vulnerability summary (Python & JavaScript) + - โœ“ Explanation of what was fixed and why + - โœ“ Documentation of vulnerabilities NOT fixed and reasoning + - โœ“ Validation results included + - โœ“ Files changed listed + - โœ“ Appropriate labels (security, dependencies) +โœ“ PR assigned to appropriate reviewers --- ## Decision Tree: When to Stop Iterating +**For Git Workflow**: +- Base branch: Use staging if it exists, otherwise main, otherwise master +- Branch creation: Always create `security-updates-dependencies` branch +- PR creation: Always create PR after all work is done +- Never force-push to base branches + **For Python (pip audit):** - Stop when: All CRITICAL and HIGH CVEs are fixed, and all tests pass -- Exception: Document and justify any LOW severity CVEs left unfixed +- Exception: Document and justify any LOW severity CVEs left unfixed in PR description - Never commit unresolved CRITICAL vulnerabilities +- Document reasoning in PR for any vulnerabilities that cannot be fixed **For JavaScript/Bun:** -- Stop when: All CRITICAL and HIGH vulnerabilities are fixed -- Exception: Can accept LOW severity if no fix is available -- Never commit unresolved CRITICAL vulnerabilities +- Stop when: All CRITICAL and HIGH vulnerabilities are analyzed +- If resolved: Great! Document in PR +- If not resolved: Document thoroughly WHY they remain (transitive deps, upstream waiting, etc.) +- Never commit unresolved CRITICAL vulnerabilities without clear mitigation path +- Document transitive/dev-only vulnerabilities in PR for transparency **Version Conflicts:** - Iterate max 3-5 times; if no solution, escalate to maintainers - Consider alternative packages if maintainer is unresponsive +- Document all conflict resolution attempts in commit messages + +**PR Creation Requirements**: +- Title: Use `security: ` format +- Description: Use comprehensive template covering all findings +- Method: Use available agent tools (`create-pull-request`, `github-pull-request_create_pull_request`) or GitHub CLI +- Base branch: Use detected base branch (staging/main/master) +- Head branch: Always `security-updates-dependencies` +- Labels: Always add `security` and `dependencies` labels +- Reviewers: Assign to security or ops team if applicable --- @@ -312,20 +552,35 @@ Once bun.lock is updated and all packages installed: | Task | Command/File | |------|---------| +| Detect base branch | `git branch -r \| grep -E 'origin/staging\|origin/main\|origin/master'` | +| Create feature branch | `git checkout -b security-updates-dependencies` | | Scan Python | `./scripts/hooks/run-pip-audit.sh` | | View ignored CVEs | `pyproject.toml` โ†’ `[tool.pip-audit]` `ignore` | | Lock Python deps | `uv lock` | | Install Python deps | `uv sync` | | Run Python tests | `uv run pytest` | +| Commit Python changes | `git add pyproject.toml uv.lock && git commit -m "security: ..."` | | Scan JavaScript | `bun audit` | | Update JS deps | `bun update` โ†’ `bun install` | | Run frontend | `bun run dev` | | Run JS tests | `bun run test` | +| Commit JS changes | `git add package.json bun.lock && git commit -m "security: ..."` | +| Push branch | `git push origin security-updates-dependencies` | +| Create PR (CLI) | `gh pr create --title "..." --body "..." --base ` | +| View branch status | `git status` | +| View commits | `git log --oneline -5` | --- ## Anti-Patterns to Avoid +โŒ **Don't** work directly on staging/main/master branches; always use feature branch +โŒ **Don't** forget to pull latest before creating feature branch +โŒ **Don't** force-push the feature branch without good reason +โŒ **Don't** skip the PR; commit and push directly to main is anti-pattern +โŒ **Don't** create PR with generic description; use comprehensive template +โŒ **Don't** forget to document why vulnerabilities were NOT fixed +โŒ **Don't** merge PRs without security review โŒ **Don't** update everything at once; update prioritized vulnerabilities โŒ **Don't** skip tests; always validate after dependency updates โŒ **Don't** ignore CRITICAL vulnerabilities; fix them or mitigate with security controls @@ -340,20 +595,33 @@ Once bun.lock is updated and all packages installed: ## Success Criteria A successful vulnerability remediation: -1. **All CRITICAL vulnerabilities are fixed** -2. **All HIGH vulnerabilities are fixed** -3. **All code tests pass** (Python + JavaScript, if applicable) -4. **All applications start without errors** -5. **Dependency versions are locked in version control** (`uv.lock`, `bun.lock`) -6. **Changes are committed with clear messaging** about fixed CVEs +1. **Git workflow completed**: Feature branch created, all changes committed, branch pushed to remote +2. **All CRITICAL vulnerabilities are fixed** +3. **All HIGH vulnerabilities are fixed or well-documented** +4. **All code tests pass** (Python + JavaScript, if applicable) +5. **All applications start without errors** +6. **Dependency versions are locked in version control** (`uv.lock`, `bun.lock`) +7. **Pull Request created** with: + - Clear title describing the security updates + - Comprehensive description covering: + - What vulnerabilities were fixed (with CVE IDs and versions) + - Why vulnerabilities were NOT fixed (if any) + - Validation results and test outcomes + - Files changed + - Appropriate labels (`security`, `dependencies`) + - Assigned to relevant reviewers +8. **Documentation complete**: All decisions, conflicts, and resolutions explained --- ## Example Prompts to Use This Skill -- "Fix all vulnerabilities in the Python dependencies" -- "Run pip audit and npm audit, then update any critical CVEs" -- "Clean up old ignored CVEs in pip-audit and update them if fixes exist" -- "Update all dependencies to latest non-breaking versions" -- "Resolve the version conflict between package-a and package-b" -- "Validate that the app still works after the latest security updates" +- "Fix all vulnerabilities in the Python dependencies and create a PR" +- "Run security audits on all dependencies and update critical CVEs" +- "Scan for CVEs, fix what we can, and open a PR with detailed findings" +- "Update dependencies to fix security vulnerabilities with automated branch and PR" +- "Clean up old ignored CVEs and update them if fixes exist, then create PR" +- "Security audit: scan Python and JavaScript, fix vulnerabilities, create PR to staging" +- "Create feature branch for security updates, fix dependencies, and open pull request" +- "Resolve the version conflict between package-a and package-b and create security PR" +- "Validate that the app still works after security updates and propose PR" From 6539588fb856ceece48a0c9756b343e41d6b6e3d Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Fri, 17 Apr 2026 15:54:26 +0200 Subject: [PATCH 128/199] refactor: update PR process to include breaking changes and detailed summary guidelines --- .../update-dependencies-fix-vulnerabilities/SKILL.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md b/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md index 96492f84..1bc2ae58 100644 --- a/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md +++ b/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md @@ -347,7 +347,7 @@ Use commit message format: `security: ` --- -## Phase 3: Pull Request and Summary +## Phase 3: Pull Request and Summary ### Step 3a: Push Branch to Remote @@ -357,18 +357,24 @@ git push origin security-updates-dependencies ### Step 3b: Create Pull Request with Detailed Summary -Create a PR from `security-updates-dependencies` to the base branch (staging/main/master) with a comprehensive description covering: +Create a PR from `security-updates-dependencies` to the base branch (staging/main/master) with a comprehensive description. #### PR Title Format ``` security: fix vulnerability updates in Python and JavaScript dependencies ``` +**Note**: If major version bumps or breaking changes were required, prefix the title with `[breaking]`: +`security: [breaking] fix critical vulnerability updates with breaking changes` #### PR Description Template ```markdown ## Security Update Summary +### Major / Breaking Changes + +- **package-name**: Updated from 1.x to 2.x. (Describe breaking change or required code updates) + ### Python Vulnerabilities Fixed - **pypdf**: Updated from 6.10.0 โ†’ 6.10.2 - Fixed: GHSA-jj6c-8h6c-hppx (DoS via malformed PDFs) @@ -524,6 +530,7 @@ gh pr create \ - Exception: Document and justify any LOW severity CVEs left unfixed in PR description - Never commit unresolved CRITICAL vulnerabilities - Document reasoning in PR for any vulnerabilities that cannot be fixed +- **Breaking Changes**: If a fix requires a major version bump or breaking change, ensure the PR title starts with `[breaking]` and the `Major / Breaking Changes` section of the description clearly lists these updates. **For JavaScript/Bun:** - Stop when: All CRITICAL and HIGH vulnerabilities are analyzed From 1c0eb808e420647d30ead8bedc90e0b83f1c3a41 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 20 Apr 2026 16:22:48 +0200 Subject: [PATCH 129/199] refactor: update secrets baseline filename and remove old baseline file --- .pre-commit-config.yaml | 5 +++-- .secrets.baseline => .secret.baseline | 0 2 files changed, 3 insertions(+), 2 deletions(-) rename .secrets.baseline => .secret.baseline (100%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8943be2b..44e456bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: rev: v1.5.0 hooks: - id: detect-secrets - args: ["--baseline", ".secrets.baseline"] + args: ["--baseline", ".secret.baseline"] exclude: ^tests/ # Notebook/text sync with jupytext @@ -66,7 +66,8 @@ repos: hooks: - id: mypy name: Type check with mypy - args: ["--config-file=pyproject.toml"] + args: [ "--config-file=pyproject.toml" ] + pass_filenames: false # Modernize Python syntax - repo: https://github.com/asottile/pyupgrade diff --git a/.secrets.baseline b/.secret.baseline similarity index 100% rename from .secrets.baseline rename to .secret.baseline From 969eeca95d4c5833fee53cc36ec38042471c8a5a Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 22 Apr 2026 13:30:45 +0200 Subject: [PATCH 130/199] - Add project audit skill - Align docs and naming Co-authored-by: Copilot --- .../audit-python-template-compliance/SKILL.md | 77 +++ .../assets/audit-report-template.md | 53 ++ .../references/audit-procedure.md | 205 +++++++ .../references/template-baseline.md | 194 ++++++ .../references/template-config-settings.md | 558 ++++++++++++++++++ .github/copilot-instructions.md | 29 +- .gitignore | 4 - .secret.baseline | 2 +- .vscode/extensions.json | 1 - README.md | 26 +- docs/0_setup.md | 2 +- docs/1_usage.md | 26 +- docs/2_secret_management.md | 2 +- docs/4_code_quality.md | 31 +- docs/5_pre_commit_hooks.md | 56 +- docs/6_pre_commit_troubleshooting.md | 16 +- scripts/setup.sh | 15 +- 17 files changed, 1191 insertions(+), 106 deletions(-) create mode 100644 .copilot/skills/audit-python-template-compliance/SKILL.md create mode 100644 .copilot/skills/audit-python-template-compliance/assets/audit-report-template.md create mode 100644 .copilot/skills/audit-python-template-compliance/references/audit-procedure.md create mode 100644 .copilot/skills/audit-python-template-compliance/references/template-baseline.md create mode 100644 .copilot/skills/audit-python-template-compliance/references/template-config-settings.md diff --git a/.copilot/skills/audit-python-template-compliance/SKILL.md b/.copilot/skills/audit-python-template-compliance/SKILL.md new file mode 100644 index 00000000..3901767e --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/SKILL.md @@ -0,0 +1,77 @@ +--- +name: audit-python-template-compliance +description: 'Audit a Python repository against the TECHLETES python_basic_template and bring it into compliance. Use when you need to compare a repo to the template, review drift in pyproject/pre-commit/CI/devcontainer/docs/secrets workflow, remediate gaps, rerun the audit, validate the repo, and produce a compliance report.' +argument-hint: 'Target repository path or repo name, plus any approved exceptions from the template' +user-invocable: true +--- + +# Python Template Compliance Audit + +Use this skill to audit a target repository against the TECHLETES Python basic +template, align the repo to the template where appropriate, re-audit, run the +available validation commands, and finish with a concise audit report. + +## Use This Skill When + +- A repository should conform to the TECHLETES Python template but predates it. +- You need to detect and fix drift in setup, tooling, quality gates, docs, or + secret-handling workflows. +- You want a structured compliance pass that ends with remediation and + verification instead of a checklist only. + +## Do Not Use This Skill When + +- The target repository is not primarily a Python repository. +- The target repository is intentionally on a different baseline and the user + has not asked to migrate it toward this template. +- The task is only to review or explain differences without making changes. + +## Scope Rules + +1. Confirm the target repository and record any approved exceptions before + changing files. +2. Treat executable configuration as the source of truth when prose docs drift. +3. Generalize template-specific values instead of copying them literally. + Package names, repo URLs, excluded directories, cache volume names, and + secret references must match the target repository. +4. Fix root causes first. Do not patch around validation failures with + one-off exceptions unless the user explicitly approves the deviation. + +## Required References + +- Read the template baseline in [template-baseline](./references/template-baseline.md). +- Use the concrete values in [template-config-settings](./references/template-config-settings.md) + when the audit needs exact tool settings instead of capability-level checks. +- Follow the execution sequence in [audit-procedure](./references/audit-procedure.md). +- Use the final structure in [audit-report-template](./assets/audit-report-template.md). + +## Deliverables + +By the end of the run, produce all of the following when feasible: + +1. A concrete audit of the target repo against the template baseline. +2. A remediation plan for every non-compliant item. +3. Implemented fixes, executed in parallel where independence is real. +4. A second audit confirming whether the repo is now compliant. +5. A validation pass using the repo's available quality gates and startup paths. +6. A final audit report summarizing findings, changes, verification, and any + approved remaining deviations. + +## Working Style + +- Start from the closest controlling files: `pyproject.toml`, + `.pre-commit-config.yaml`, `.github/workflows/`, `.devcontainer/`, + `README.md`, `docs/`, first-party packages, and test entry points. +- Keep the first edits small and validate immediately. +- Parallelize only independent slices, such as docs versus CI copy edits, after + you know they do not share the same controlling config. +- If the target repo lacks a required baseline feature, add the minimal + compliant implementation and then validate it. + +## Completion Standard + +The skill is complete only after you have either: + +- Brought the repository into compliance and verified it, or +- Identified the exact blockers, documented the remaining deviations, and + reported which validations could not be completed. diff --git a/.copilot/skills/audit-python-template-compliance/assets/audit-report-template.md b/.copilot/skills/audit-python-template-compliance/assets/audit-report-template.md new file mode 100644 index 00000000..5dacdfca --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/audit-report-template.md @@ -0,0 +1,53 @@ +# Audit Report Template + +## Scope + +- Target repository: +- Branch: +- Expected alignment level: +- Approved deviations: + +## Baseline Summary + +- Template baseline used: +- Source-of-truth files reviewed: + +## Findings Before Remediation + +| Area | Expected | Actual | Severity | Planned action | +| --- | --- | --- | --- | --- | +| | | | | | + +## Changes Applied + +| Area | Files changed | Root-cause fix | Validation after edit | +| --- | --- | --- | --- | +| | | | | + +## Re-Audit Result + +| Area | Final state | Notes | +| --- | --- | --- | +| | Fixed / approved deviation / blocked / not applicable | | + +## Validation Run + +| Command or check | Result | Notes | +| --- | --- | --- | +| `uv sync` | | | +| `uv run pre-commit run --all-files` | | | +| `uv run pytest` | | | +| `uv run mypy . --config-file=pyproject.toml` | | | + +Add service startup checks or other repo-specific validations below the table. + +## Final Compliance Status + +- Compliant: +- Remaining deviations: +- Blockers: + +## Follow-Up + +- Immediate follow-up items: +- Optional future improvements: diff --git a/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md b/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md new file mode 100644 index 00000000..88ed93f4 --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md @@ -0,0 +1,205 @@ +# Audit Procedure + +Follow this sequence to audit a target repository against the TECHLETES Python +template, remediate gaps, and verify the result. + +## Outcome + +Complete all of these when feasible: + +1. Capture the repo's current state against the template baseline. +2. Build a concrete remediation plan for every non-compliant item. +3. Implement fixes with minimal, root-cause edits. +4. Re-audit after changes. +5. Run the available validations and service startups. +6. Produce a final audit report. + +## Step 1. Confirm Scope and Exceptions + +Before editing: + +1. Identify the target repository and working branch. +2. Confirm whether the repo is expected to align fully or partially with the + template. +3. Record approved deviations up front. +4. If the repo is not primarily Python or does not use `uv` as its intended + package workflow, stop and ask whether this template is the correct baseline. + +## Step 2. Collect Evidence + +Read the controlling surfaces first, in parallel where possible. + +Primary evidence set: + +- `pyproject.toml` +- `uv.lock` +- `.pre-commit-config.yaml` +- `.github/workflows/` +- `.github/dependabot.yml` +- `.github/pull_request_template.md` +- `.github/ISSUE_TEMPLATE/` +- `.github/copilot-instructions.md` +- `.devcontainer/` +- `.vscode/` +- `README.md` +- `CONTRIBUTING.md` +- `docs/` +- `.envrc` +- `.detect-secrets.yaml` +- `.gitignore` +- `.gitattributes` +- secret helper modules +- package `__init__.py` files for runtime typing setup +- tests and smoke checks +- helper scripts such as setup, new-branch, and audit wrappers +- Copilot assets under `.copilot/` when present + +Use `template-config-settings.md` to compare concrete values for tool settings +such as pytest `addopts`, mypy strictness flags, Ruff rule sets, Bandit skips, +pre-commit hook revisions, and devcontainer editor settings. + +If a baseline file is missing, treat that as a finding rather than continuing as +though the capability exists. + +## Step 3. Build the Audit Matrix + +Create a finding entry for each baseline area: + +- Dependency management and packaging +- Typing and runtime type enforcement +- Pre-commit hooks and security gates +- Secret handling +- CI validation +- Devcontainer support +- Repo scripts and developer workflow +- Documentation completeness and accuracy + +For each finding, record: + +- Expected baseline +- Expected concrete setting values when relevant +- Actual state +- Severity: blocking, major, minor, or informational +- Whether it is auto-fixable +- The smallest validating command or check +- Whether the gap is a real defect or an approved deviation + +## Step 4. Create the Alignment Plan + +Plan fixes in dependency order. + +Recommended order: + +1. Core project model + `pyproject.toml`, package layout, `uv.lock` +2. Quality and security gates + `.pre-commit-config.yaml`, pip-audit, Bandit, detect-secrets +3. Automation and environment setup + `scripts/`, `.github/workflows/`, `.devcontainer/`, `.envrc` +4. Code and tests + runtime typing hooks, helper modules, missing tests +5. Documentation + `README.md`, `CONTRIBUTING.md`, `docs/` + +Parallelize only independent slices. Examples: + +- Docs can often be updated in parallel with test additions after the final + executable behavior is known. +- CI and devcontainer copy edits may be parallel only if neither depends on the + same config changes still in flight. + +Do not parallelize changes that share a controlling file or a single validation +surface such as `pyproject.toml` plus `uv.lock`. + +## Step 5. Execute the Plan + +Implement the smallest set of changes that makes the target repo comply with the +baseline. + +Rules: + +1. Generalize template details to the target repo. +2. Prefer adding missing baseline capabilities over documenting gaps away. +3. If the target repo already has an equivalent mechanism, align and document it + instead of replacing it unnecessarily. +4. Keep edits reversible and validate the touched slice immediately. + +### Focused Validation After Each Edit Slice + +After the first substantive edit, run the cheapest meaningful check before doing +more exploration. + +Examples: + +- If `pyproject.toml` changes: run `uv lock` and then `uv sync` if needed. +- If `.pre-commit-config.yaml` changes: run the affected hook or + `uv run pre-commit run --all-files` if narrow hook execution is awkward. +- If Python source changes: run the nearest relevant test, then mypy if the + change affects typing. +- If docs change only: executable validation is not available, so verify the + docs against the final config and move on. + +If a validation fails, repair that same slice first. Do not widen scope until +the current slice is understood. + +## Step 6. Re-Audit + +Re-run the same compliance matrix after the fixes land. + +Mark each finding as one of: + +- Fixed +- Approved deviation +- Blocked +- Not applicable + +The re-audit is not complete until every original finding has one of those +states. + +## Step 7. Full Verification + +Run the target repo's available validation commands. For repos aligned to this +template, the standard sequence is: + +```bash +uv sync +uv run pre-commit run --all-files +uv run pytest +uv run mypy . --config-file=pyproject.toml +``` + +Also run any additional checks that the repo exposes, such as: + +- service startup commands for Flask, FastAPI, Streamlit, or worker processes +- notebook kernel or import smoke checks +- devcontainer smoke validation when Docker/devcontainer tooling is available + +If a service exists, start it long enough to confirm it boots successfully. If +the environment prevents that, record the exact blocker. + +## Step 8. Report + +Finish with the structure in +[audit-report-template](../assets/audit-report-template.md). + +The report should include: + +- Scope and approved exceptions +- Findings before remediation +- Changes applied +- Re-audit result +- Validation commands run and results +- Remaining blockers or approved deviations + +## Decision Rules + +Use these rules when the target repo does not match the template cleanly. + +- Missing baseline feature: add it unless the user approved an exception. +- Different but equivalent implementation: keep it if it meets the same intent, + then document the equivalence. +- Docs contradict automation: trust the executable config first. +- Target repo contains stale template placeholders: replace them with target repo + values rather than copying more template text. +- Validation unavailable due to environment: report it explicitly, with the + exact command that could not be run. diff --git a/.copilot/skills/audit-python-template-compliance/references/template-baseline.md b/.copilot/skills/audit-python-template-compliance/references/template-baseline.md new file mode 100644 index 00000000..0a121411 --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/references/template-baseline.md @@ -0,0 +1,194 @@ +# TECHLETES Python Template Baseline + +Use this reference to decide what the target repository should look like when +it is aligned with the TECHLETES Python basic template. + +## Source-of-Truth Order + +When template sources disagree, prefer them in this order: + +1. Executable config and automation + `pyproject.toml`, `.pre-commit-config.yaml`, `.github/workflows/`, + `.devcontainer/`, `scripts/` +2. Contributor-facing contract docs + `README.md`, `CONTRIBUTING.md`, `docs/` +3. Example code and tests + +Do not copy stale prose over working automation. Align the docs to the actual +behavior unless the automation is clearly outdated and should be changed. + +When you need exact comparison values for tool configuration, use +`template-config-settings.md` alongside this baseline. + +## Baseline Areas + +### 1. Platform and Dependency Management + +- Python baseline is `>=3.12,<4.0`. +- Dependency management is `uv`-first. +- Dependencies are declared in `pyproject.toml`. +- Locked dependency state is committed in `uv.lock`. +- Version constraints use bounded ranges such as `>=X.Y.Z,=61.0", "wheel", "pip>=26.0,<27.0.0", "uv>=0.9.25,<1.0.0"]` +- `build-backend = "setuptools.build_meta"` + +`[project]` + +- `name = "python-basic-template"` +- `version = "0.1.0"` +- `description = "A basic Python project template with full DevOps tooling support."` +- `readme = "README.md"` +- `requires-python = ">=3.12,<4.0"` +- `keywords = ["devops", "template", "python"]` +- authors and maintainers are populated +- classifiers target Python 3.12 + +`[project.urls]` + +- `Homepage = "https://github.com/TECHLETES/python_basic_template"` +- `Repository = "https://github.com/TECHLETES/python_basic_template"` +- `Issues = "https://github.com/TECHLETES/python_basic_template/issues"` + +### Dependencies + +`[project].dependencies` + +- `flask>=3.1.3,<4.0.0` +- `requests>=2.32.0,<3.0.0` +- `urllib3>=2.6.0,<3.0.0` +- `werkzeug>=3.1.6,<4.0.0` +- `filelock>=3.20.0,<4.0.0` + +`[dependency-groups].dev` + +- `pre-commit>=4.5.0,<5.0.0` +- `detect-secrets>=1.5.0,<2.0.0` +- `nbstripout>=0.8.0,<1.0.0` +- `ipykernel>=6.29.0,<7.0.0` +- `pytest>=9.0.3,<10.0.0` +- `pytest-cov>=7.0.0,<8.0.0` +- `black>=25.12.0,<27.0.0` +- `mypy>=1.19.0,<2.0.0` +- `ruff>=0.14.0,<1.0.0` +- `beartype>=0.22.0,<1.0.0` +- `pytest-beartype>=0.2.0,<1.0.0` +- `pip-audit>=2.10.0,<3.0.0` +- `rich>=14.2.0,<15.0.0` +- `pyupgrade>=3.15.0,<4.0.0` +- `codespell>=2.2.0,<3.0.0` +- `bandit>=1.7.0,<2.0.0` +- `pydocstyle>=6.3.0,<7.0.0` +- `genbadge[coverage]>=1.1.0,<2.0.0` +- `testcontainers>=4.8.0` +- `pillow~=12.2` + +`[dependency-groups].extra` + +- present and currently empty except for a commented example + +### setuptools package discovery + +`[tool.setuptools.packages.find]` + +- `where = ["."]` +- `include = ["utils*", "example*"]` +- `exclude = ["tests*"]` + +### Black + +`[tool.black]` + +- `line-length = 88` +- `target-version = ["py312"]` + +### pytest + +`[tool.pytest]` + +- `minversion = "9.0.3"` +- `addopts = [` + `"--strict-markers"`, + `"--strict-config"`, + `"--cov=utils"`, + `"--cov=example"`, + `"--cov-report=term-missing"`, + `"--cov-report=html"`, + `"--cov-report=xml"`, + `"--beartype-packages=utils,example"` + `]` +- `testpaths = ["tests"]` +- `python_files = ["test_*.py", "*_test.py"]` +- `python_classes = ["Test*"]` +- `python_functions = ["test_*"]` +- `pythonpath = ["."]` + +### coverage.py + +`[tool.coverage.run]` + +- `source = ["utils", "example"]` +- `omit = [` + `"*/__pycache__/*"`, + `"*/tests/*"`, + `"*/example/*"`, + `"*/.venv/*"`, + `"*/venv/*"`, + `"*/__init__.py"`, + `"setup.py"` + `]` + +`[tool.coverage.report]` + +- `exclude_lines = [` + `"pragma: no cover"`, + `"def __repr__"`, + `"if self.debug:"`, + `"if settings.DEBUG"`, + `"raise AssertionError"`, + `"raise NotImplementedError"`, + `"if 0:"`, + `"if __name__ == .__main__.:"`, + `"class .*\\bProtocol\\):"`, + `"@(abc\\.)?abstractmethod"`, + `"except ImportError"`, + `"pass"` + `]` + +### mypy + +`[tool.mypy]` + +- `python_version = "3.12"` +- `ignore_missing_imports = true` +- `follow_imports = "silent"` +- `warn_unused_ignores = true` +- `warn_return_any = true` +- `warn_unreachable = true` +- `no_implicit_optional = true` +- `allow_redefinition = true` +- `show_column_numbers = true` +- `pretty = true` +- `warn_unused_configs = true` +- `disallow_untyped_defs = true` +- `disallow_incomplete_defs = true` +- `files = ["example"]` +- `exclude = '''(?x)(^migrations/|^notebooks/|^tests/|^\.venv/|^venv/)'''` +- `plugins = []` + +### Ruff + +`[tool.ruff]` + +- `target-version = "py312"` +- `line-length = 88` +- `fix = true` +- `respect-gitignore = true` +- `extend-exclude = ["migrations"]` +- `lint.dummy-variable-rgx = "^_|^__|^pytest$"` + +`lint.select = ["E", "F", "I", "B", "UP"]` + +- `E` for pycodestyle +- `F` for pyflakes +- `I` for import sorting +- `B` for bugbear +- `UP` for pyupgrade + +`lint.ignore = ["B008", "E501"]` + +### Bandit + +`[tool.bandit]` + +- `exclude_dirs = ["tests", ".venv"]` +- `skips = ["B101", "B601"]` + +### pip-audit + +`[tool.pip-audit]` + +- `ignore = ["CVE-2026-1703"]` + +### uv + +`[tool.uv]` + +- `required-version = ">=0.9.25,<1.0.0"` + +## .pre-commit-config.yaml + +### Hook set and revisions + +- local `nbstripout-autoadd` + `entry: uv run scripts/hooks/nbstripout-autoadd.sh` + `language: system` + `types: [jupyter]` +- `psf/black` at `25.9.0` + `language_version: python3.12` + `types: [python]` +- `pre-commit/pre-commit-hooks` at `v6.0.0` + includes `trailing-whitespace`, `end-of-file-fixer`, `check-toml`, + `check-json`, `check-symlinks`, `check-yaml`, `check-added-large-files`, + `check-merge-conflict`, `detect-private-key`, `pretty-format-json` +- `end-of-file-fixer` excludes `coverage\.svg$` +- `pretty-format-json` uses `args: ["--autofix"]` and excludes `\.ipynb$` +- `pypa/pip-audit` at `v2.10.0` + `args: ["--progress-spinner", "off", "--desc"]` + `entry: uv run bash scripts/hooks/run-pip-audit.sh` + `language: system` + `stages: [pre-commit]` +- `Yelp/detect-secrets` at `v1.5.0` + `args: ["--baseline", ".secret.baseline"]` + `exclude: ^tests/` +- `mwouts/jupytext` at `v1.19.1` + `args: [--sync]` + `require_serial: true` +- `mirrors-mypy` at `v1.19.1` + `args: ["--config-file=pyproject.toml"]` + `pass_filenames: false` +- `asottile/pyupgrade` at `v3.21.2` + `args: [--py312-plus]` + `types: [python]` +- `astral-sh/ruff-pre-commit` at `v0.15.4` + `args: [--fix]` + `types: [python]` +- `PyCQA/bandit` at `1.9.4` + `args: [-ll, --skip, B101]` + `exclude: ^tests/` + `types: [python]` +- `PyCQA/pydocstyle` at `6.3.0` + `exclude: ^(tests/|example/)` + `types: [python]` +- local `pytest` + `entry: uv run pytest --cov --cov-fail-under=80 --beartype-packages=utils,example` + `language: system` + `pass_filenames: false` + `types: [python]` + `stages: [pre-commit]` + +## Devcontainer Configuration + +### .devcontainer/devcontainer.json + +- build context: `..` +- dockerfile: `Dockerfile` +- `containerEnv.PRE_COMMIT_HOME = "/home/vscode/.cache/pre-commit"` +- `containerEnv.UV_CACHE_DIR = "/home/vscode/.cache/uv"` +- `containerEnv.UV_LINK_MODE = "copy"` +- `containerEnv.UV_PYTHON_DOWNLOADS = "never"` +- VS Code extensions installed in container: + `ms-python.python`, `ms-python.vscode-pylance`, + `ms-python.mypy-type-checker`, `charliermarsh.ruff`, + `ms-toolsai.jupyter` +- VS Code settings in container: + `jupyter.jupyterServerType = "local"` + `python.defaultInterpreterPath = "${workspaceFolder}/.venv/bin/python"` + `python.terminal.activateEnvironment = true` + `python.testing.pytestArgs = ["."]` + `python.testing.pytestEnabled = true` + `python.testing.unittestEnabled = false` + `ruff.enable = true` +- mounts: + `source=python-template-uv-cache,target=/home/vscode/.cache/uv,type=volume` + `source=python-template-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume` +- `name = "python-basic-template"` +- `postCreateCommand = "bash .devcontainer/post-create.sh"` +- `remoteUser = "vscode"` +- `updateRemoteUserUID = true` +- `workspaceFolder = "/workspaces/${localWorkspaceFolderBasename}"` + +### .devcontainer/Dockerfile + +- base image: `mcr.microsoft.com/devcontainers/python:1-3.12-bookworm` +- `ARG USERNAME=vscode` +- `ARG UV_VERSION=0.9.25` +- installs `uv` into `/usr/local/bin` +- sets: + `PIP_DISABLE_PIP_VERSION_CHECK=1` + `PYTHONDONTWRITEBYTECODE=1` +- pre-creates cache directories for `uv` and `pre-commit` + +### .devcontainer/post-create.sh + +- changes to repo root +- prints `uv --version` and `python --version` +- runs `uv sync --frozen` +- runs `uv run pre-commit install --install-hooks` when `.git` exists +- checks `uv run python -c "import ipykernel"` +- reports whether `op` exists + +## GitHub Automation and Metadata + +### .github/workflows/ci.yml + +- workflow name: `CI` +- triggers: + pull requests to `main` and `staging` + pushes to `main` and `staging` +- `permissions.contents = write` + +`lint-and-test` job: + +- runs on `ubuntu-latest` +- `PRE_COMMIT_HOME = ${{ github.workspace }}/.pre-commit-cache` +- Python matrix: `['3.12']` +- uses: + `actions/checkout@v6` + `actions/setup-python@v6` + `astral-sh/setup-uv@v7` + `actions/cache@v4` + `stefanzweifel/git-auto-commit-action@v7` +- installs dependencies with `uv sync` +- caches pre-commit hooks using `.pre-commit-config.yaml` hash +- runs `uv run pre-commit run --all-files` +- runs `uv run pytest --cov --cov-report=term --cov-report=xml` +- on pull requests, generates `coverage.svg` with + `uv run genbadge coverage -i coverage.xml -o coverage.svg` +- on pull requests, auto-commits `coverage.svg` back to the PR branch with + commit message `chore: update coverage badge` + +`devcontainer-smoke` job: + +- runs on `ubuntu-latest` +- uses `actions/checkout@v6` +- uses `docker/setup-buildx-action@v3` +- uses `devcontainers/ci@v0.3` +- builds from `.devcontainer/devcontainer.json` +- verifies: + `.venv` exists + `.git/hooks/pre-commit` is executable + `uv --version` succeeds + `.venv/bin/python --version` succeeds + +### .github/dependabot.yml + +- `version: 2` +- update ecosystems: + `uv` in `/` daily, open PR limit `0` + `npm` in `/web` daily, open PR limit `0`, commit prefix `deps`, + `versioning-strategy: increase-if-necessary` + `docker` in `/` daily, open PR limit `0` + `github-actions` in `/` daily, open PR limit `0` + +### .github/pull_request_template.md + +- sections for: + description + type of change + contributor checklist + security considerations + testing + screenshots + related issues +- checklist explicitly asks for documentation updates, self-review, warning-free + changes, added tests, local test pass, and security review + +### .github/ISSUE_TEMPLATE/ + +`config.yml` + +- `blank_issues_enabled: false` +- contact links for: + discussions + private security advisory reporting + documentation + support/Q&A discussions + +`bug-report.md` + +- default title prefix: `[Bug] ` +- default label: `bug` +- collects reproduction steps, expected vs actual behavior, screenshots/logs, + environment, additional context, and acceptance criteria + +`feature-request.md` + +- default title prefix: `[FEATURE] ` +- default label: `enhancement` +- collects problem statement, solution, alternatives, use case, implementation + ideas, additional context, and priority/impact + +`docs_update.md` + +- default title prefix: `[DOCS] ` +- default label: `documentation` +- collects target location, issue type, current content, suggested changes, + context, audience, priority, related issues, and desired format + +## Workspace Editor Configuration + +### .vscode/extensions.json + +- recommended extensions: + `ms-vscode-remote.remote-containers` + `ms-azuretools.vscode-docker` + `ms-python.python` + `ms-python.vscode-pylance` + `ms-python.mypy-type-checker` + `charliermarsh.ruff` + `ms-toolsai.jupyter` + `astral-sh.ty` + `ms-python.autopep8` + `ms-python.black-formatter` + `ms-python.debugpy` + `mgesbert.python-path` + `docker.docker` + `dbaeumer.vscode-eslint` + `tamasfe.even-better-toml` + `cweijan.vscode-excel` + `GitHub.copilot-chat` + `github.vscode-github-actions` + `GitHub.vscode-pull-request-github` + `ms-azuretools.vscode-azure-github-copilot` + `mtxr.sqltools` + `mtxr.sqltools-driver-mysql` + `mtxr.sqltools-driver-pg` + `mtxr.sqltools-driver-sqlite` + `ms-ossdata.vscode-pgsql` + +### .vscode/settings.json + +- `mypy-type-checker.args = ["--config-file=pyproject.toml"]` +- `mypy-type-checker.cwd = "${workspaceFolder}"` +- `mypy-type-checker.importStrategy = "fromEnvironment"` +- `mypy-type-checker.reportingScope = "workspace"` +- `python.analysis.typeCheckingMode = "standard"` +- `python.defaultInterpreterPath = "${workspaceFolder}/.venv/bin/python"` +- `python.testing.pytestArgs = ["."]` +- `python.testing.pytestEnabled = true` +- `python.testing.unittestEnabled = false` +- `ruff.enable = true` + +## Shell Bootstrap and Repo Hygiene + +### .envrc + +- loads `.env` if present using `set -a` and `source .env` +- exports: + `PYTHONPATH=.` + `VENV_DIR=".venv"` + `PYPROJECT="pyproject.toml"` +- auto-activates `.venv/bin/activate` when `.venv` exists + +### .detect-secrets.yaml + +- excludes files matching `^tests/` + +### .gitignore + +- ignores Python caches and bytecode +- ignores virtual environments: `.venv/`, `venv/`, `env/`, `ENV/` +- ignores notebook checkpoints +- ignores mypy, Ruff, pytest, coverage, tox, nox caches and outputs +- ignores build and distribution artifacts +- ignores `.env` and `.env.*` +- ignores `.direnv/` + +### .gitattributes + +- `* text=auto` +- `*.sh eol=lf` +- `*.ps1 eol=crlf` +- `*.py eol=lf` +- `*.ipynb -text` + +## Scripts and Repo Workflows + +### scripts/setup.sh + +- shebang: `#!/usr/bin/env bash` +- strict mode: `set -euo pipefail` +- exports default env vars: + `VENV_DIR=.venv` + `PYPROJECT=pyproject.toml` +- verifies/install system prerequisites: Python 3, pip, git, uv, direnv, curl +- for `apt-get` hosts, installs Docker and Docker Compose if missing +- runs `uv lock` then `uv sync` +- configures locales to `en_US.UTF-8` +- appends `eval "$(direnv hook bash)"` to `~/.bashrc` when absent +- runs `direnv allow` if `.envrc` exists +- installs pre-commit hooks, runs `pre-commit autoupdate`, then + `pre-commit run --all-files` +- creates a secrets baseline with `detect-secrets scan > .secret.baseline` +- audits that baseline with `detect-secrets audit .secret.baseline` +- validates imports via `importlib.import_module("utils.secrets")` + +### scripts/new-branch.sh + +- usage: `./scripts/new-branch.sh ` +- supported types: `feature`, `fix`, `docs`, `ui`, `devops` +- lowercases and slugifies description into branch name +- branch format: `/` +- prunes `origin` remote-tracking branches +- deletes local branches already merged into `main`, excluding `main` +- creates the new branch from current HEAD with `git checkout -b` +- prints suggested follow-up commit and push commands + +### scripts/install-copilot-agents.sh + +- installs repo-shared Copilot assets into the user environment +- modes: `--copy` or `--link` +- replacement control: `--force` +- scope control: `--agents-only` or `--skills-only` +- env overrides: + `COPILOT_AGENTS_DIR` + `COPILOT_SKILLS_DIR` +- agents source: `.copilot/agents/*.agent.md` +- skills source: `.copilot/skills/*/SKILL.md` plus the full skill directory + +## Copilot Customization Surface + +### .github/copilot-instructions.md + +- states the repo is a TECHLETES Python template with `uv`, 1Password, + beartype plus mypy, pre-commit, and CI/CD expectations +- establishes four core rules: + secrets management + dependency management + mandatory type hints + committing from an active virtual environment +- documents daily commands and template-specific conventions for AI-assisted + coding + +### .copilot/ + +- contains versioned shared agents under `.copilot/agents/` +- contains versioned shared skills under `.copilot/skills/` +- install path is managed by `scripts/install-copilot-agents.sh` + +## Governance Files + +### CODE_OF_CONDUCT.md + +- repository includes a Contributor Covenant based code of conduct adapted for + TECHLETES internal collaboration + +## How To Use During Audits + +- Compare concrete values only where the target repo is expected to match the + template directly. +- Generalize package names, repo names, paths, coverage targets, and cache + identifiers to the target repo. +- If the target repo has an equivalent but intentionally different setting, + record that as either an approved deviation or an equivalence decision. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b611b65f..f2f5cd56 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -59,8 +59,8 @@ A production-ready Python project template for TECHLETES employees. Features mod ```bash git clone https://github.com/TECHLETES/python_basic_template.git cd python_basic_template -./scripts/setup.sh # Linux/macOS -.\scripts\setup.ps1 # Windows +./scripts/setup.sh # Linux host setup +# On Windows or macOS, prefer the devcontainer workflow from docs/quickstart.md ``` ### Activate Virtual Environment (Every Session) @@ -76,12 +76,12 @@ direnv allow # If using direnv |------|---------| | **Install/update deps** | `uv sync` | | **Add new package** | Edit `pyproject.toml`, then `uv lock && uv sync` | -| **Run tests** | `pytest` or `make test` | -| **Run tests with coverage** | `pytest --cov` | +| **Run tests** | `uv run pytest` | +| **Run tests with coverage** | `uv run pytest --cov` | | **Format code** | `uv run black .` | | **Lint and fix** | `uv run ruff check . --fix` | | **Type check** | `uv run mypy . --config-file=pyproject.toml` | -| **Pre-commit all files** | `pre-commit run --all-files` | +| **Pre-commit all files** | `uv run pre-commit run --all-files` | | **Load secret** | `from utils.secrets import get_secret; api_key = get_secret("op://...")` | --- @@ -103,16 +103,15 @@ tests/ โ””โ”€โ”€ test_*.py # Unit tests (prefix: test_) scripts/ -โ”œโ”€โ”€ setup.sh / setup.ps1 # Environment initialization +โ”œโ”€โ”€ setup.sh # Linux host environment initialization +โ”œโ”€โ”€ install-copilot-agents.sh # Installs repo-shared Copilot agents and skills โ”œโ”€โ”€ new-branch.sh # Git workflow helper โ””โ”€โ”€ hooks/ # Pre-commit hook scripts - โ”œโ”€โ”€ black-autoformat.sh โ”œโ”€โ”€ nbstripout-autoadd.sh # Auto-strip notebook outputs - โ”œโ”€โ”€ precommit_pytest.sh # Run tests on commit - โ””โ”€โ”€ verify-lockfile.sh # Ensure uv.lock is valid + โ””โ”€โ”€ run-pip-audit.sh # pip-audit wrapper used in hooks and CI .github/ -โ”œโ”€โ”€ workflows/ci.yml # GitHub Actions: lint, type check, test, coverage +โ”œโ”€โ”€ workflows/ci.yml # GitHub Actions: pre-commit, tests, coverage, devcontainer smoke โ”œโ”€โ”€ pull_request_template.md # PR checklist (includes security review) โ””โ”€โ”€ dependabot.yml # Automated dependency updates @@ -166,7 +165,7 @@ uv.lock # Locked dependency versions (auto-generated - **Branch naming:** Use `./scripts/new-branch.sh` to create feature branches - **Commits:** Must pass all pre-commit checks (linting, tests, type checking, security) - **Pre-commit hooks:** Run automatically on `git commit` (venv must be active) -- **Manual pre-check:** `pre-commit run --all-files` before committing +- **Manual pre-check:** `uv run pre-commit run --all-files` before committing - **Force push only if:** Rebasing your own unpushed branch --- @@ -251,12 +250,12 @@ uv run bash scripts/hooks/run-pip-audit.sh --progress-spinner off --desc ### Debug Pre-Commit Hook Failures ```bash # Run a specific hook manually -pre-commit run black --all-files -pre-commit run mypy --all-files -pre-commit run bandit --all-files +uv run pre-commit run black --all-files +uv run pre-commit run mypy --all-files +uv run pre-commit run bandit --all-files # Run all hooks -pre-commit run --all-files +uv run pre-commit run --all-files # See logs if a Python tool failed # Edit the file to fix the issue, then commit again diff --git a/.gitignore b/.gitignore index 6cd56d82..48a2be52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,3 @@ -# Don't use requirement.in or manually modified requirements.txt! -# We use pip-tools to manage dependencies. It reads and builds dpendencies from pyproject.toml. -requirements.in - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.secret.baseline b/.secret.baseline index b091e406..f3a59ef3 100644 --- a/.secret.baseline +++ b/.secret.baseline @@ -92,7 +92,7 @@ }, { "path": "detect_secrets.filters.common.is_baseline_file", - "filename": ".secrets.baseline" + "filename": ".secret.baseline" }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", diff --git a/.vscode/extensions.json b/.vscode/extensions.json index fee6b70a..182171be 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -8,7 +8,6 @@ "charliermarsh.ruff", "ms-toolsai.jupyter", "astral-sh.ty", - "charliermarsh.ruff", "ms-python.autopep8", "ms-python.black-formatter", "ms-python.debugpy", diff --git a/README.md b/README.md index f8af90f7..da4c8a61 100644 --- a/README.md +++ b/README.md @@ -115,13 +115,10 @@ python_basic_template/ โ”œโ”€โ”€ scripts/ # Setup and utility scripts โ”‚ โ”œโ”€โ”€ setup.sh โ”‚ โ”œโ”€โ”€ install-copilot-agents.sh # Installs repo-shared Copilot agents into ~/.copilot/agents -โ”‚ โ”œโ”€โ”€ run-pip-audit.sh # pip-audit wrapper used in hooks and CI โ”‚ โ”œโ”€โ”€ new-branch.sh โ”‚ โ””โ”€โ”€ hooks/ -โ”‚ โ”œโ”€โ”€ black-autoformat.sh โ”‚ โ”œโ”€โ”€ nbstripout-autoadd.sh -โ”‚ โ”œโ”€โ”€ precommit_pytest.sh -โ”‚ โ””โ”€โ”€ verify-lockfile.sh +โ”‚ โ””โ”€โ”€ run-pip-audit.sh # pip-audit wrapper used in hooks and CI โ”œโ”€โ”€ utils/ # Utility modules โ”‚ โ”œโ”€โ”€ __init__.py โ”‚ โ””โ”€โ”€ secrets.py @@ -130,7 +127,7 @@ python_basic_template/ โ”œโ”€โ”€ uv.lock # Locked dependency versions (auto-generated) โ”œโ”€โ”€ .pre-commit-config.yaml # Pre-commit hooks config โ”œโ”€โ”€ .envrc # direnv environment config -โ”œโ”€โ”€ .secrets.baseline # Secret detection baseline +โ”œโ”€โ”€ .secret.baseline # Secret detection baseline โ”œโ”€โ”€ CODE_OF_CONDUCT.md # Contributor code of conduct โ”œโ”€โ”€ CONTRIBUTING.md # Contribution guidelines โ””โ”€โ”€ README.md # Project overview (this file) @@ -257,20 +254,19 @@ Use [docs/quickstart.md](docs/quickstart.md) to choose between the devcontainer ```bash # Typing check -mypy . +uv run mypy . --config-file=pyproject.toml # Format code -black . +uv run black . # Lint and fix code -ruff check . --fix -mypy . +uv run ruff check . --fix # Run tests -pytest +uv run pytest # Run all pre-commit checks -pre-commit run --all-files +uv run pre-commit run --all-files ``` ## ๐Ÿงช Testing @@ -279,13 +275,13 @@ Run tests with pytest: ```bash # Run all tests -pytest +uv run pytest # Run with coverage -pytest --cov +uv run pytest --cov # Run specific test -pytest tests/test_specific.py +uv run pytest tests/test_specific.py ``` ## ๐Ÿ“ Configuration @@ -298,7 +294,7 @@ pytest tests/test_specific.py | `uv.lock` | Locked dependency versions (auto-generated by uv) | | `.pre-commit-config.yaml` | Pre-commit hooks configuration | | `.envrc` | Environment variables (direnv) | -| `.secrets.baseline` | Secret detection baseline | +| `.secret.baseline` | Secret detection baseline | ### Tool Configuration diff --git a/docs/0_setup.md b/docs/0_setup.md index 0e3bbf76..eb0d8a7a 100644 --- a/docs/0_setup.md +++ b/docs/0_setup.md @@ -111,7 +111,7 @@ The `./scripts/setup.sh` script automates: 1. **System requirements check** โ€“ Verifies Python 3.12+, Git, `uv`, `direnv`, and other required tools, installing missing packages with `apt-get`, `dnf`, or `yum` when possible 2. **Virtual environment and dependencies** โ€“ Materializes `.venv/`, runs `uv lock`, and runs `uv sync` 3. **Pre-commit hooks** โ€“ Installs Git hooks, runs `pre-commit autoupdate`, and runs `pre-commit run --all-files` -4. **Secrets baseline** โ€“ Creates `.secrets.baseline` with `detect-secrets` +4. **Secrets baseline** โ€“ Creates `.secret.baseline` with `detect-secrets` 5. **direnv configuration** โ€“ Appends the bash hook to `~/.bashrc` and runs `direnv allow` when `.envrc` exists 6. **System configuration** โ€“ Configures locales and, on `apt-get`-based Linux, installs Docker and Docker Compose if they are missing diff --git a/docs/1_usage.md b/docs/1_usage.md index bc5adb69..b353f3f7 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -18,22 +18,22 @@ This guide covers the day-to-day development workflow, including dependency mana 3. **Run Quality Checks (Optional)**: Before committing, you can run the same checks that the pre-commit hooks use: ```bash # Format code - black . + uv run black . # Lint for errors and sort imports - ruff check . --fix + uv run ruff check . --fix # Check static types - mypy . + uv run mypy . --config-file=pyproject.toml ``` 4. **Run Tests**: Ensure all existing functionality still works. ```bash # Run all tests - pytest + uv run pytest # Run tests with a coverage report - pytest --cov + uv run pytest --cov ``` 5. **Commit Code**: Stage your files and commit them. @@ -199,14 +199,14 @@ If a hook fails, your commit will be aborted. **This is not an error with your c * **Common Error**: `Secrets detected!` followed by a list of findings. * **How to Fix**: 1. **NEVER IGNORE THIS HOOK** unless you are 100% certain it is a false positive. - 2. Remove the detected secret from your code. Use the project's secret management system instead (see `docs/1_secret_management.md`). + 2. Remove the detected secret from your code. Use the project's secret management system instead (see `docs/2_secret_management.md`). 3. **For False Positives**: If the detected string is not a secret, you must update the baseline file to tell `detect-secrets` to ignore it in the future: ```bash # Re-scan the project and overwrite the baseline - detect-secrets scan > .secrets.baseline + detect-secrets scan > .secret.baseline # Add the new baseline to your staged files - git add .secrets.baseline + git add .secret.baseline ``` 4. Commit again. @@ -226,12 +226,12 @@ If a hook fails, your commit will be aborted. **This is not an error with your c ## ๐Ÿ—๏ธ Managing Dependencies -This project uses a hybrid approach where dependencies are declared in `pyproject.toml` and locked to `requirements.txt` files for reproducibility. +This project uses `uv` with `pyproject.toml` as the dependency source of truth and `uv.lock` for reproducible installs. * **To Add a Production Dependency**: - 1. Add the package name (e.g., `"pandas"`) to the `dependencies` list in `pyproject.toml`. - 2. Run `uv lock && uv sync` to update the lockfile + 1. Add the package name (e.g., `"pandas>=2.0.0,<3.0.0"`) to the `dependencies` list in `pyproject.toml`. + 2. Run `uv lock && uv sync` to update `uv.lock` and the environment. * **To Add a Development-Only Dependency**: - 1. Add the package name (e.g., `"ipykernel"`) to the `dev` list under `[project.optional-dependencies]` in `pyproject.toml`. - 2. Run `uv lock && uv sync` + 1. Add the package name (e.g., `"ipykernel>=6.29.0,<7.0.0"`) to `[dependency-groups].dev` in `pyproject.toml`. + 2. Run `uv lock && uv sync`. diff --git a/docs/2_secret_management.md b/docs/2_secret_management.md index dc7f8c0b..8edd01ae 100644 --- a/docs/2_secret_management.md +++ b/docs/2_secret_management.md @@ -155,5 +155,5 @@ This way, CI runners receive secrets securely from GitHub and never see 1Passwor [Compromised secret action plan](https://www.notion.so/Compromised-secret-action-plan-24517d03144d805eba09e65cd5fe6a49?pvs=21) -- **Audit** with `detect-secrets scan > .secrets.baseline` after updates. +- **Audit** with `detect-secrets scan > .secret.baseline` after updates. - Maintain a clear **playbook** in `SECURITY.md` for incident response. diff --git a/docs/4_code_quality.md b/docs/4_code_quality.md index cfe2b1f7..9e3bf64e 100644 --- a/docs/4_code_quality.md +++ b/docs/4_code_quality.md @@ -18,7 +18,7 @@ Mypy checks type correctness at development and commit time, catching bugs befor **Run manually:** ```bash -mypy . --config-file=pyproject.toml +uv run mypy . --config-file=pyproject.toml ``` **What it checks:** @@ -41,13 +41,13 @@ Beartype enforces type correctness at runtime for annotated functions and classe Beartype is automatically enabled during pytest execution through the `--beartype-packages` configuration in `pyproject.toml`: ```bash -pytest # Beartype is automatically enabled for utils and example packages +uv run pytest # Beartype is automatically enabled for utils and example packages ``` To run tests without beartype (for performance): ```bash -RUNTIME_TYPECHECK=false pytest +RUNTIME_TYPECHECK=false uv run pytest ``` ## 4.2 Quality @@ -61,12 +61,12 @@ Ruff provides fast linting and catches many code issues before commit. - Replaces flake8 and pyupgrade in a single fast tool - Configured via `pyproject.toml` with enabled rules: E (pycodestyle), F (pyflakes), I (import sorting), B (bugbear), UP (pyupgrade) - Automatically fixes issues when possible -- Target version set to Python 3.11+ with 88-character line length +- Target version set to Python 3.12 with 88-character line length **Run manually:** ```bash -ruff check . --fix +uv run ruff check . --fix ``` **What it catches:** @@ -83,12 +83,12 @@ Black is the opinionated formatter used to ensure all code is styled consistentl - Used alongside Ruff for full formatting consistency - Auto-formats code during pre-commit hooks - Configured in `pyproject.toml` with 88-character line length and Python 3.12 target -- Integrated with the pre-commit `black-autoformat` hook +- Integrated with the pre-commit `black` hook **Run manually:** ```bash -black . +uv run black . ``` **What it does:** @@ -102,7 +102,7 @@ black . All tools are configured in `pyproject.toml`: - **MyPy**: Strict mode enabled with Python 3.12 target, excludes migrations and notebooks -- **Ruff**: Targets Python 3.11+ with rules E, F, I, B, UP enabled, respects .gitignore +- **Ruff**: Targets Python 3.12 with rules E, F, I, B, UP enabled, respects .gitignore - **Black**: 88-character line length, Python 3.12 target version - **Beartype**: Automatically enabled for `utils` and `example` packages during testing - **Pytest**: Configured with coverage reporting and beartype integration @@ -122,20 +122,17 @@ To run all quality checks manually: ```bash # Format code -black . +uv run black . # Lint and fix issues -ruff check . --fix +uv run ruff check . --fix # Type check -mypy . +uv run mypy . --config-file=pyproject.toml # Run tests with beartype -pytest -``` - -Or use pre-commit to run all configured hooks: +uv run pytest -```bash -pre-commit run --all-files +# Or use pre-commit to run all configured hooks: +uv run pre-commit run --all-files ``` diff --git a/docs/5_pre_commit_hooks.md b/docs/5_pre_commit_hooks.md index d55f1738..3a27ea88 100644 --- a/docs/5_pre_commit_hooks.md +++ b/docs/5_pre_commit_hooks.md @@ -12,13 +12,22 @@ The following hooks are currently configured: | Hook ID | Purpose | | --- | --- | -| `prevent-manual-requirements-edits` | Block manual edits to `requirements.txt` | | `nbstripout-autoadd` | Strip notebook outputs and auto-stage cleaned files | -| `black-autoformat` | Auto-format Python code with Black | +| `black` | Auto-format Python code with Black | +| `trailing-whitespace` and `end-of-file-fixer` | Basic text hygiene | +| `check-toml`, `check-json`, `check-yaml`, `check-symlinks` | Validate common config files | +| `check-added-large-files` and `check-merge-conflict` | Prevent common Git mistakes | +| `detect-private-key` | Block private keys | +| `pretty-format-json` | Reformat JSON files | +| `pip-audit` | Check Python dependencies for known vulnerabilities | | `detect-secrets` | Detect and prevent committing secrets | | `jupytext` | Sync `.ipynb` and `.py` notebook pairs | | `mypy` | Type check Python code with mypy | +| `pyupgrade` | Modernize Python syntax for Python 3.12 | | `ruff` | Lint Python code with Ruff (includes import sorting) | +| `bandit` | Run Python security linting | +| `pydocstyle` | Validate docstring conventions | +| `pytest` | Run tests with coverage in pre-commit | The configuration is stored in the `.pre-commit-config.yaml` file at the project root. @@ -30,13 +39,17 @@ The configuration is stored in the `.pre-commit-config.yaml` file at the project From now on, every time you run `git commit`, `pre-commit` will: -1. Block manual edits to `requirements.txt` -2. Strip outputs from notebooks using `nbstripout-autoadd` -3. Auto-format Python code with Black +1. Strip outputs from notebooks using `nbstripout-autoadd` +2. Auto-format Python code with Black +3. Run text and config hygiene checks 4. Detect any committed secrets with `detect-secrets` -5. Sync `.ipynb` and `.py` files using `jupytext` -6. Type check Python code with `mypy` -7. Lint Python code with `ruff` (includes import sorting) +5. Audit dependencies with `pip-audit` +6. Sync `.ipynb` and `.py` files using `jupytext` +7. Type check Python code with `mypy` +8. Modernize syntax with `pyupgrade` +9. Lint and sort imports with `ruff` +10. Run security and docstring checks with `bandit` and `pydocstyle` +11. Run pytest with coverage If any hook fails, the commit is blocked until the issue is resolved. @@ -56,16 +69,16 @@ The `nbstripout-autoadd` hook is a **local wrapper** that: ### 3.3.2 Code Formatting and Quality -#### Black Auto-formatting +#### Black Formatting -The `black-autoformat` hook automatically formats Python code to ensure consistent style across the project. Black is an opinionated code formatter that: +The `black` hook automatically formats Python code to ensure consistent style across the project. Black is an opinionated code formatter that: - Ensures consistent indentation and spacing - Formats imports and function definitions - Handles line length and string formatting - Automatically fixes formatting issues -If Black makes changes to your files, they will be automatically staged for commit. +If Black makes changes to your files, re-stage them before retrying the commit. #### Type Checking with MyPy @@ -97,22 +110,25 @@ To avoid accidentally committing secrets (API keys, tokens, etc.), the project u If you intentionally add or rotate secrets: ```bash -detect-secrets scan > .secrets.baseline -git add .secrets.baseline +detect-secrets scan > .secret.baseline +git add .secret.baseline ``` -Make sure `.secrets.baseline` stays committed and up to date. +Make sure `.secret.baseline` stays committed and up to date. --- ### 3.3.4 Enforcing Dependency Workflow -The custom `prevent-manual-requirements-edits` hook ensures that the lock file is only modified via `uv lock` and never edited manually. +This repository does not use `requirements.txt` as the primary dependency contract. Dependencies are declared in `pyproject.toml` and locked in `uv.lock`, and the pre-commit pipeline enforces safety through `pip-audit` plus the repo's `uv` workflow. To add a package: ```bash -uv add package-name +# 1. Edit pyproject.toml +# 2. Regenerate the lock file and sync the environment +uv lock +uv sync ``` --- @@ -150,14 +166,12 @@ This is useful to verify changes or fix all issues in one go. ### 3.5.2 `nbstripout: command not found` -This means `nbstripout` isnโ€™t installed in the hookโ€™s environment. Make sure your `.pre-commit-config.yaml` includes: +This usually means the project environment has not been synced yet. This repo uses a local hook script that runs through `uv`, so ensure dependencies are installed: -```yaml - additional_dependencies: [nbstripout] +```bash +uv sync ``` -in the `nbstripout-autoadd` hook definition. - ### 3.5.3 MyPy Type Checking Issues If you encounter type checking errors: diff --git a/docs/6_pre_commit_troubleshooting.md b/docs/6_pre_commit_troubleshooting.md index 26b6b6b0..5170c970 100644 --- a/docs/6_pre_commit_troubleshooting.md +++ b/docs/6_pre_commit_troubleshooting.md @@ -311,11 +311,11 @@ example = "exec(code)" # noqa: S303 **Solutions:** ```bash # Audit and update baseline -detect-secrets scan > .secrets.baseline -detect-secrets audit .secrets.baseline +detect-secrets scan > .secret.baseline +detect-secrets audit .secret.baseline # If certain patterns are always false positives -detect-secrets scan --all-files > .secrets.baseline +detect-secrets scan --all-files > .secret.baseline ``` **Problem:** Baseline file corrupted or out of sync @@ -323,20 +323,20 @@ detect-secrets scan --all-files > .secrets.baseline **Fixes:** ```bash # Regenerate baseline -rm .secrets.baseline -detect-secrets scan > .secrets.baseline +rm .secret.baseline +detect-secrets scan > .secret.baseline # Verify it's valid JSON -python -m json.tool .secrets.baseline > /dev/null +python -m json.tool .secret.baseline > /dev/null ``` **Problem:** Valid test credentials flagged as secrets -**Solution:** Add exemption in `.secrets.baseline`: +**Solution:** Add exemption in `.secret.baseline`: ```bash # Or in hook, exclude test files - id: detect-secrets - args: ["--baseline", ".secrets.baseline"] + args: ["--baseline", ".secret.baseline"] exclude: ^tests/|\.txt$ ``` diff --git a/scripts/setup.sh b/scripts/setup.sh index b2af7d26..9f4d60a6 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -4,9 +4,6 @@ set -euo pipefail # configuration export VENV_DIR="${VENV_DIR:-.venv}" export PYPROJECT="${PYPROJECT:-pyproject.toml}" -export PROD_LOCK="${PROD_LOCK:-requirements.txt}" -export DEV_LOCK="${DEV_LOCK:-requirements-dev.txt}" -export SLIM_LOCK="${SLIM_LOCK:-requirements-slim.txt}" # ui helpers GREEN='\033[0;32m' @@ -225,13 +222,13 @@ setup_secrets() { section "secret management setup" step "initializing secrets baseline" - if [ -f ".secrets.baseline" ]; then - warn "existing .secrets.baseline found. backing up" - cp .secrets.baseline .secrets.baseline.bak + if [ -f ".secret.baseline" ]; then + warn "existing .secret.baseline found. backing up" + cp .secret.baseline .secret.baseline.bak fi - $VENV_DIR/bin/detect-secrets scan > .secrets.baseline + $VENV_DIR/bin/detect-secrets scan > .secret.baseline success "secrets baseline created" - $VENV_DIR/bin/detect-secrets audit .secrets.baseline || warn "manual review of baseline recommended" + $VENV_DIR/bin/detect-secrets audit .secret.baseline || warn "manual review of baseline recommended" success "secrets configuration verified" } @@ -256,7 +253,7 @@ print_completion() { echo " โ€ข $VENV_DIR/" echo " โ€ข uv.lock (all dependencies locked and synced)" echo " โ€ข pre commit hooks" - echo " โ€ข secret baseline (.secrets.baseline)" + echo " โ€ข secret baseline (.secret.baseline)" echo " โ€ข direnv" echo -e "\n${BLUE}next steps:${RESET}" echo " 1. restart terminal or run: source ~/.bashrc" From ee007370392c332aba33a3a147279a6ba865cb88 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Thu, 23 Apr 2026 11:25:17 +0200 Subject: [PATCH 131/199] Update repo audit skill Co-authored-by: Copilot --- .../audit-python-template-compliance/SKILL.md | 8 +- .../config-template/.detect-secrets.yaml | 3 + .../config-template/.devcontainer/Dockerfile | 15 + .../.devcontainer/devcontainer.json | 43 ++ .../.devcontainer/post-create.sh | 27 + .../assets/config-template/.envrc | 15 + .../assets/config-template/.gitattributes | 5 + .../config-template/.github/dependabot.yml | 34 + .../config-template/.github/workflows/ci.yml | 90 +++ .../assets/config-template/.gitignore | 106 ++++ .../config-template/.pre-commit-config.yaml | 120 ++++ .../config-template/.vscode/extensions.json | 29 + .../config-template/.vscode/settings.json | 16 + .../assets/config-template/pyproject.toml | 210 +++++++ .../scripts/hooks/nbstripout-autoadd.sh | 14 + .../scripts/hooks/run-pip-audit.sh | 27 + .../references/audit-procedure.md | 13 +- .../references/template-baseline.md | 9 +- .../references/template-config-settings.md | 582 ++---------------- 19 files changed, 820 insertions(+), 546 deletions(-) create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.detect-secrets.yaml create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/post-create.sh create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.envrc create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.gitattributes create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.github/dependabot.yml create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.github/workflows/ci.yml create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.gitignore create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/settings.json create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/pyproject.toml create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/nbstripout-autoadd.sh create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/run-pip-audit.sh diff --git a/.copilot/skills/audit-python-template-compliance/SKILL.md b/.copilot/skills/audit-python-template-compliance/SKILL.md index 3901767e..dc074aea 100644 --- a/.copilot/skills/audit-python-template-compliance/SKILL.md +++ b/.copilot/skills/audit-python-template-compliance/SKILL.md @@ -40,8 +40,9 @@ available validation commands, and finish with a concise audit report. ## Required References - Read the template baseline in [template-baseline](./references/template-baseline.md). -- Use the concrete values in [template-config-settings](./references/template-config-settings.md) - when the audit needs exact tool settings instead of capability-level checks. +- Use the asset index in [template-config-settings](./references/template-config-settings.md) + and its linked literal templates when the audit needs exact tool settings + instead of capability-level checks. - Follow the execution sequence in [audit-procedure](./references/audit-procedure.md). - Use the final structure in [audit-report-template](./assets/audit-report-template.md). @@ -61,7 +62,8 @@ By the end of the run, produce all of the following when feasible: - Start from the closest controlling files: `pyproject.toml`, `.pre-commit-config.yaml`, `.github/workflows/`, `.devcontainer/`, - `README.md`, `docs/`, first-party packages, and test entry points. + `scripts/hooks/`, `README.md`, `docs/`, first-party packages, and test entry + points. - Keep the first edits small and validate immediately. - Parallelize only independent slices, such as docs versus CI copy edits, after you know they do not share the same controlling config. diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.detect-secrets.yaml b/.copilot/skills/audit-python-template-compliance/assets/config-template/.detect-secrets.yaml new file mode 100644 index 00000000..c73c9a1e --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.detect-secrets.yaml @@ -0,0 +1,3 @@ +exclude: + files: + - ^tests/ diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile new file mode 100644 index 00000000..404bfc23 --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/devcontainers/python:1-3.12-bookworm + +ARG USERNAME=vscode +ARG UV_VERSION=0.9.25 + +ADD https://astral.sh/uv/${UV_VERSION}/install.sh /tmp/uv-installer.sh + +RUN env UV_UNMANAGED_INSTALL="/usr/local/bin" sh /tmp/uv-installer.sh \ + && rm /tmp/uv-installer.sh + +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PYTHONDONTWRITEBYTECODE=1 + +RUN mkdir -p /home/${USERNAME}/.cache/uv /home/${USERNAME}/.cache/pre-commit \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.cache diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json new file mode 100644 index 00000000..c0062762 --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +{ + "build": { + "context": "..", + "dockerfile": "Dockerfile" + }, + "containerEnv": { + "PRE_COMMIT_HOME": "/home/vscode/.cache/pre-commit", + "UV_CACHE_DIR": "/home/vscode/.cache/uv", + "UV_LINK_MODE": "copy", + "UV_PYTHON_DOWNLOADS": "never" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.mypy-type-checker", + "charliermarsh.ruff", + "ms-toolsai.jupyter" + ], + "settings": { + "jupyter.jupyterServerType": "local", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.terminal.activateEnvironment": true, + "python.testing.pytestArgs": [ + "." + ], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "ruff.enable": true + } + } + }, + "mounts": [ + "source=your-repo-uv-cache,target=/home/vscode/.cache/uv,type=volume", + "source=your-repo-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume" + ], + "name": "your-project-name", + "postCreateCommand": "bash .devcontainer/post-create.sh", + "remoteUser": "vscode", + "updateRemoteUserUID": true, + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}" +} diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/post-create.sh b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/post-create.sh new file mode 100644 index 00000000..6e517d41 --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/post-create.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${repo_root}" + +echo "Bootstrapping the container workspace with uv..." +uv --version +python --version + +uv sync --frozen + +if [[ -d .git ]]; then + uv run pre-commit install --install-hooks +fi + +if ! uv run python -c "import ipykernel" >/dev/null 2>&1; then + echo "Notebook execution in the container uses the local Python environment and needs ipykernel available there." +fi + +if command -v op >/dev/null 2>&1; then + echo "1Password CLI detected. Run 'op account list' if you need secret-backed examples." +else + echo "1Password CLI is optional and not installed in this container." +fi + +echo "For Windows contributors, keep this repo inside the WSL filesystem before reopening it in the container for the best bind-mount performance." diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.envrc b/.copilot/skills/audit-python-template-compliance/assets/config-template/.envrc new file mode 100644 index 00000000..1ee6f0cb --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.envrc @@ -0,0 +1,15 @@ +# Load environment variables from .env if it exists +if [ -f .env ]; then + set -a + source .env + set +a +fi + +export PYTHONPATH=. +export VENV_DIR=".venv" +export PYPROJECT="pyproject.toml" + +# Set a virtualenv automatically if venv exists +if [ -d "$VENV_DIR" ]; then + source $VENV_DIR/bin/activate +fi diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitattributes b/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitattributes new file mode 100644 index 00000000..88f68334 --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.sh eol=lf +*.ps1 eol=crlf +*.py eol=lf +*.ipynb -text diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/dependabot.yml b/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/dependabot.yml new file mode 100644 index 00000000..f013d7dd --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/dependabot.yml @@ -0,0 +1,34 @@ +version: 2 + +updates: + # Python dependencies (uv project) + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 0 + + # Web dependencies via package.json (Bun-compatible through npm ecosystem) + # Remove or adjust this entry if the target repo does not ship a /web app. + - package-ecosystem: "npm" + directory: "/web" + schedule: + interval: "daily" + open-pull-requests-limit: 0 + commit-message: + prefix: "deps" + versioning-strategy: increase-if-necessary + + # Dockerfile base images + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 0 + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 0 diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/workflows/ci.yml b/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/workflows/ci.yml new file mode 100644 index 00000000..fce4094f --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +"on": + pull_request: + branches: + - main + - staging + push: + branches: + - main + - staging + +permissions: + contents: write + +jobs: + lint-and-test: + name: Lint & Test + runs-on: ubuntu-latest + env: + PRE_COMMIT_HOME: ${{ github.workspace }}/.pre-commit-cache + strategy: + matrix: + python-version: ['3.12'] + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + persist-credentials: true + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Install dependencies + run: uv sync + + - name: Cache pre-commit hooks + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.pre-commit-cache + key: pre-commit-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + pre-commit-${{ runner.os }}-${{ matrix.python-version }}- + + - name: Run pre-commit hooks + run: uv run pre-commit run --all-files + + - name: Run tests with coverage + run: uv run pytest --cov --cov-report=term --cov-report=xml + + - name: Generate coverage badge + if: github.event_name == 'pull_request' + run: uv run genbadge coverage -i coverage.xml -o coverage.svg + + - name: Commit badge + uses: stefanzweifel/git-auto-commit-action@v7 + if: github.event_name == 'pull_request' + with: + commit_message: "chore: update coverage badge" + file_pattern: coverage.svg + branch: ${{ github.head_ref }} + + devcontainer-smoke: + name: Devcontainer Smoke + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and bootstrap devcontainer + uses: devcontainers/ci@v0.3 + with: + configFile: .devcontainer/devcontainer.json + push: never + runCmd: | + test -d .venv + test -x .git/hooks/pre-commit + uv --version + .venv/bin/python --version diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitignore b/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitignore new file mode 100644 index 00000000..48a2be52 --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitignore @@ -0,0 +1,106 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ + +# CSV and Excel files +*.csv +*.xlsx +*.xls +*.xlsm +*.xlsb +*.ods + +# Jupyter Notebook checkpoints +.ipynb_checkpoints/ + +# VSCode settings +# .vscode/ + +# PyCharm settings +.idea/ + +# macOS files +.DS_Store + +# mypy type checker cache +.mypy_cache/ +.dmypy.json +dmypy.json + +.ruff_cache/ + +# Pyre type checker +.pyre/ + +# pytype +.pytype/ + +# Pytest cache +.pytest_cache/ + +# Coverage reports +htmlcov/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml + +# Test output +test-results/ + +# Build / distribution +build/ +dist/ +*.egg-info/ +.eggs/ +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage tools +.tox/ +.nox/ + +# Python wheels +*.whl + +# Editable installs +*.egg-link + +# dotenv files (if used for secrets) +.env +.env.* + +# Poetry +poetry.lock + +# PDM +__pypackages__/ + +# PyInstaller +*.spec + +# Log files +*.log + +# Temporary files +*.bak +*.swp +*.swo +*~ + +# Local notebooks +notebooks-local/ + +# direnv +.direnv/ diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml b/.copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml new file mode 100644 index 00000000..b8e93b9b --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml @@ -0,0 +1,120 @@ +repos: + - repo: local + hooks: + # Strip notebook outputs + - id: nbstripout-autoadd + name: nbstripout (auto-add notebooks) + entry: uv run scripts/hooks/nbstripout-autoadd.sh + language: system + types: [jupyter] + + # Python formatter + - repo: https://github.com/psf/black + rev: 25.9.0 + hooks: + - id: black + name: Black (python formatter) + language_version: python3.12 + types: [python] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + exclude: coverage\.svg$ + - id: check-toml + - id: check-json + - id: check-symlinks + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict + - id: detect-private-key + - id: pretty-format-json + args: ["--autofix"] + exclude: \.ipynb$ + + # Dependency vulnerability scanning with uv.lock + - repo: https://github.com/pypa/pip-audit + rev: v2.10.0 + hooks: + - id: pip-audit + args: ["--progress-spinner", "off", "--desc"] + entry: uv run bash scripts/hooks/run-pip-audit.sh + language: system + stages: [pre-commit] + + # Secrets scanning + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ["--baseline", ".secret.baseline"] + exclude: ^tests/ + + # Notebook/text sync with jupytext + - repo: https://github.com/mwouts/jupytext + rev: v1.19.1 + hooks: + - id: jupytext + args: [--sync] + require_serial: true + + # Type checking with mypy + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.19.1 + hooks: + - id: mypy + name: Type check with mypy + args: ["--config-file=pyproject.toml"] + pass_filenames: false + + # Modernize Python syntax + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + name: pyupgrade (modernize Python syntax) + args: [--py312-plus] + types: [python] + + # Linting and formatting with ruff + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.4 + hooks: + - id: ruff + name: Ruff (linting + import sorting + formatting) + args: [--fix] + types: [python] + + # Security scanning + - repo: https://github.com/PyCQA/bandit + rev: 1.9.4 + hooks: + - id: bandit + name: bandit (security check) + args: [-ll, --skip, B101] + exclude: ^tests/ + types: [python] + + # Docstring validation + - repo: https://github.com/PyCQA/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + name: pydocstyle (docstring validation) + # Adjust this regex for the target repo's non-production directories. + exclude: ^(tests/|example/) + types: [python] + + # Testing with pytest (locally managed by uv) + - repo: local + hooks: + - id: pytest + name: Run pytest with coverage + # Replace the beartype package list for the target repo when needed. + entry: uv run pytest --cov --cov-fail-under=80 --beartype-packages=your_package + language: system + pass_filenames: false + types: [python] + stages: [pre-commit] diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json b/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json new file mode 100644 index 00000000..182171be --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json @@ -0,0 +1,29 @@ +{ + "recommendations": [ + "ms-vscode-remote.remote-containers", + "ms-azuretools.vscode-docker", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.mypy-type-checker", + "charliermarsh.ruff", + "ms-toolsai.jupyter", + "astral-sh.ty", + "ms-python.autopep8", + "ms-python.black-formatter", + "ms-python.debugpy", + "mgesbert.python-path", + "docker.docker", + "dbaeumer.vscode-eslint", + "tamasfe.even-better-toml", + "cweijan.vscode-excel", + "GitHub.copilot-chat", + "github.vscode-github-actions", + "GitHub.vscode-pull-request-github", + "ms-azuretools.vscode-azure-github-copilot", + "mtxr.sqltools", + "mtxr.sqltools-driver-mysql", + "mtxr.sqltools-driver-pg", + "mtxr.sqltools-driver-sqlite", + "ms-ossdata.vscode-pgsql" + ] +} diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/settings.json b/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/settings.json new file mode 100644 index 00000000..e6b2045f --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "mypy-type-checker.args": [ + "--config-file=pyproject.toml" + ], + "mypy-type-checker.cwd": "${workspaceFolder}", + "mypy-type-checker.importStrategy": "fromEnvironment", + "mypy-type-checker.reportingScope": "workspace", + "python.analysis.typeCheckingMode": "standard", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.testing.pytestArgs": [ + "." + ], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "ruff.enable": true +} diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/pyproject.toml b/.copilot/skills/audit-python-template-compliance/assets/config-template/pyproject.toml new file mode 100644 index 00000000..70c38352 --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/pyproject.toml @@ -0,0 +1,210 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel", "pip>=26.0,<27.0.0", "uv>=0.9.25,<1.0.0"] +build-backend = "setuptools.build_meta" + +[project.urls] +# Replace with the target repository URLs. +Homepage = "https://github.com/TECHLETES/your-repo" +Repository = "https://github.com/TECHLETES/your-repo" +Issues = "https://github.com/TECHLETES/your-repo/issues" + +[project] +# Replace project metadata for the target repository. +name = "your-project-name" +version = "0.1.0" +description = "Your project description." +readme = "README.md" +requires-python = ">=3.12,<4.0" +keywords = ["devops", "template", "python"] +authors = [{ name = "Your Name", email = "you@example.com" }] +maintainers = [{ name = "Your Team", email = "team@example.com" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", +] + +# Core production dependencies required for the application to run. +# Pinned with >= and < to allow patch/minor updates while preventing major version breaks. +# Add only dependencies that are needed for the app to run in production. +dependencies = [ + # Add the target repo's runtime dependencies here. +] + +# Optional dependency groups for different environments and use cases. +# Managed by uv (PEP 735 format) and locked in uv.lock for reproducible builds. +# Install with: uv sync (all groups), uv sync --no-group dev (no dev), uv sync --only-group extra (extra only), etc. +[dependency-groups] +# Development and testing dependencies. +# Includes code quality tools, testing frameworks, and development utilities. +# Install with: uv sync (default includes dev) or uv sync --no-group dev to skip +dev = [ + "pre-commit>=4.5.0,<5.0.0", + "detect-secrets>=1.5.0,<2.0.0", + "nbstripout>=0.8.0,<1.0.0", + "ipykernel>=6.29.0,<7.0.0", + "pytest>=9.0.3,<10.0.0", + "pytest-cov>=7.0.0,<8.0.0", + "black>=25.12.0,<27.0.0", + "mypy>=1.19.0,<2.0.0", + "ruff>=0.14.0,<1.0.0", + "beartype>=0.22.0,<1.0.0", + "pytest-beartype>=0.2.0,<1.0.0", + "pip-audit>=2.10.0,<3.0.0", + "rich>=14.2.0,<15.0.0", + "pyupgrade>=3.15.0,<4.0.0", + "codespell>=2.2.0,<3.0.0", + "bandit>=1.7.0,<2.0.0", + "pydocstyle>=6.3.0,<7.0.0", + "genbadge[coverage]>=1.1.0,<2.0.0", + "testcontainers>=4.8.0", + "pillow~=12.2" +] + +# Additional production features and optional components. +# Install with: uv sync --group extra or uv sync --no-group extra +extra = [ + # Add optional production dependencies for the target repo here. +] + +[tool.setuptools.packages.find] +where = ["."] +include = [ + # Replace with the target repo's importable packages, e.g. "your_package*". +] +exclude = ["tests*"] + +[tool.black] +line-length = 88 +target-version = ["py312"] + +[tool.pytest] +minversion = "9.0.3" +addopts = [ + "--strict-markers", + "--strict-config", + # Replace with the target repo's coverage package arguments. + # "--cov=your_package", + "--cov-report=term-missing", + "--cov-report=html", + "--cov-report=xml", + # Replace with the target repo's beartype-enabled packages when used. + # "--beartype-packages=your_package", +] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +pythonpath = ["."] + +[tool.coverage.run] +source = [ + # Replace with the target repo's production packages. + # "your_package", +] +omit = [ + "*/__pycache__/*", + "*/tests/*", + "*/.venv/*", + "*/venv/*", + "*/__init__.py", + "setup.py", + # Add any other files you want to exclude from test coverage. +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", + "except ImportError", + "pass", +] + +[tool.mypy] +python_version = "3.12" + +# ignore_missing_imports for third-party libs without type hints +ignore_missing_imports = true +follow_imports = "silent" + +# strictness settings - adjust as needed +warn_unused_ignores = true +warn_return_any = true +warn_unreachable = true +no_implicit_optional = true +allow_redefinition = true +show_column_numbers = true +pretty = true +warn_unused_configs = true + +# enforce typing on own code +disallow_untyped_defs = true +disallow_incomplete_defs = true + +# Specify which folders/files to type check. +files = [ + # Replace with the target repo's packages or entry points to type check. + # "your_package", +] + +exclude = ''' +(?x)( + # Adjust these patterns for the target repo. + ^migrations/ + | ^notebooks/ + | ^tests/ + | ^\.venv/ + | ^venv/ +) +''' +plugins = [] + +[tool.ruff] +target-version = "py312" +line-length = 88 +fix = true +respect-gitignore = true +extend-exclude = [ + # Add project-specific exclusions here, e.g. "migrations". +] +lint.dummy-variable-rgx = "^_|^__|^pytest$" + +# Enable popular rule sets +lint.select = [ + "E", # pycodestyle + "F", # pyflakes + "I", # import sorting + "B", # bugbear + "UP", # pyupgrade +] + +# Rules to ignore (common ones) +lint.ignore = [ + "B008", # Do not perform function calls in argument defaults + "E501", # Line too long (Black handles this) +] + +[tool.bandit] +exclude_dirs = ["tests", ".venv"] +skips = ["B101", "B601"] + +[tool.pip-audit] +# Centralized CVE/PYSEC exclusions for pip-audit +# Used by both pre-commit hooks and CI workflows via scripts/hooks/run-pip-audit.sh +# See docs/6_pre_commit_troubleshooting.md for explanation of each exclusion +ignore = [ + "CVE-2026-1703", # Path traversal in pip when extracting malicious wheels +] + +[tool.uv] +required-version = ">=0.9.25,<1.0.0" diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/nbstripout-autoadd.sh b/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/nbstripout-autoadd.sh new file mode 100644 index 00000000..8220c4ab --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/nbstripout-autoadd.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +# Run nbstripout on all files passed by pre-commit +nbstripout "$@" + +# Re-add any modified .ipynb files to the index +modified=$(git ls-files -m '*.ipynb') || true + +if [ -n "$modified" ]; then + echo "๐Ÿ” Re-staging modified notebooks:" + echo "$modified" + git add $modified +fi diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/run-pip-audit.sh b/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/run-pip-audit.sh new file mode 100644 index 00000000..0202b037 --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/run-pip-audit.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Helper script to build pip-audit ignore arguments from pyproject.toml [tool.pip-audit] +# This ensures both pre-commit and CI use the same CVE exclusion list + +# Extract ignore list from pyproject.toml using Python +IGNORE_ARGS=$(python3 << 'EOF' +import tomllib +import sys + +try: + with open("pyproject.toml", "rb") as f: + config = tomllib.load(f) + + # Get the ignore list from [tool.pip-audit] + ignore_list = config.get("tool", {}).get("pip-audit", {}).get("ignore", []) + + # Build the arguments + args = " ".join([f"--ignore-vuln {cve}" for cve in ignore_list]) + print(args) +except Exception as e: + print(f"Error reading pyproject.toml: {e}", file=sys.stderr) + sys.exit(1) +EOF +) + +# Run pip-audit with the exclusions from pyproject.toml +pip-audit $IGNORE_ARGS "$@" diff --git a/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md b/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md index 88ed93f4..94e512e9 100644 --- a/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md +++ b/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md @@ -51,12 +51,14 @@ Primary evidence set: - secret helper modules - package `__init__.py` files for runtime typing setup - tests and smoke checks -- helper scripts such as setup, new-branch, and audit wrappers +- helper scripts such as setup, new-branch, hook wrappers under + `scripts/hooks/`, and audit wrappers - Copilot assets under `.copilot/` when present -Use `template-config-settings.md` to compare concrete values for tool settings -such as pytest `addopts`, mypy strictness flags, Ruff rule sets, Bandit skips, -pre-commit hook revisions, and devcontainer editor settings. +Use `template-config-settings.md` and its linked literal asset files to compare +concrete values for tool settings such as pytest `addopts`, mypy strictness +flags, Ruff rule sets, Bandit skips, pre-commit hook revisions, delegated hook +scripts, and devcontainer editor settings. If a baseline file is missing, treat that as a finding rather than continuing as though the capability exists. @@ -95,7 +97,8 @@ Recommended order: 2. Quality and security gates `.pre-commit-config.yaml`, pip-audit, Bandit, detect-secrets 3. Automation and environment setup - `scripts/`, `.github/workflows/`, `.devcontainer/`, `.envrc` + `scripts/`, `scripts/hooks/`, `.github/workflows/`, `.devcontainer/`, + `.envrc` 4. Code and tests runtime typing hooks, helper modules, missing tests 5. Documentation diff --git a/.copilot/skills/audit-python-template-compliance/references/template-baseline.md b/.copilot/skills/audit-python-template-compliance/references/template-baseline.md index 0a121411..18aef019 100644 --- a/.copilot/skills/audit-python-template-compliance/references/template-baseline.md +++ b/.copilot/skills/audit-python-template-compliance/references/template-baseline.md @@ -9,7 +9,7 @@ When template sources disagree, prefer them in this order: 1. Executable config and automation `pyproject.toml`, `.pre-commit-config.yaml`, `.github/workflows/`, - `.devcontainer/`, `scripts/` + `.devcontainer/`, `scripts/`, especially `scripts/hooks/` 2. Contributor-facing contract docs `README.md`, `CONTRIBUTING.md`, `docs/` 3. Example code and tests @@ -18,7 +18,8 @@ Do not copy stale prose over working automation. Align the docs to the actual behavior unless the automation is clearly outdated and should be changed. When you need exact comparison values for tool configuration, use -`template-config-settings.md` alongside this baseline. +`template-config-settings.md` and the linked literal asset templates alongside +this baseline. ## Baseline Areas @@ -66,6 +67,8 @@ When you need exact comparison values for tool configuration, use - Bandit is configured for security scanning. - pip-audit exclusions are centralized in `pyproject.toml` under `[tool.pip-audit]`, not duplicated ad hoc in hooks or CI. +- Hook wrapper scripts under `scripts/hooks/` are part of the baseline when + pre-commit or CI delegates shared logic there. - Secret scanning uses `detect-secrets` plus a committed baseline file. - Notebook cleanup and synchronization are part of the repo contract when the repo includes notebooks. @@ -173,6 +176,8 @@ exception. - `scripts/setup.sh` - `scripts/new-branch.sh` - `scripts/install-copilot-agents.sh` +- `scripts/hooks/nbstripout-autoadd.sh` +- `scripts/hooks/run-pip-audit.sh` - `.copilot/agents/` and `.copilot/skills/` when the repo ships shared Copilot assets - Repo-local first-party package initialization that enables runtime type checks diff --git a/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md b/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md index 571a6f67..ac016e87 100644 --- a/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md +++ b/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md @@ -1,558 +1,68 @@ -# Template Config Settings +# Template Config Assets -This file captures the concrete settings currently used by the TECHLETES Python -template. Use it when the audit needs exact values rather than broad baseline -expectations. +This reference now acts as an index to generalized literal config files stored +under `assets/config-template/`. -These values come from the template's executable configuration. If this file and -the repo config ever diverge, the repo config wins and this file should be -updated. +Use these asset files when you need to compare a target repository against the +template's actual config shape, syntax, comments, and default settings. -## pyproject.toml +## Generalization Rules -### Build and Package Metadata +The asset files intentionally preserve the template's literal file structure, +formatting, and most current settings. Only these classes of values are +generalized with comments or placeholders: -`[build-system]` +- runtime dependencies and optional production dependencies +- target-repository identity values such as repo URLs or project names +- package, folder, file, and regex references that should point at the target + repo rather than this template's `utils` and `example` packages +- repo-specific cache volume names or similar identifiers -- `requires = ["setuptools>=61.0", "wheel", "pip>=26.0,<27.0.0", "uv>=0.9.25,<1.0.0"]` -- `build-backend = "setuptools.build_meta"` +If an asset file and the current repository diverge, the repository file is the +source of truth and the asset should be updated. -`[project]` +## Asset Files -- `name = "python-basic-template"` -- `version = "0.1.0"` -- `description = "A basic Python project template with full DevOps tooling support."` -- `readme = "README.md"` -- `requires-python = ">=3.12,<4.0"` -- `keywords = ["devops", "template", "python"]` -- authors and maintainers are populated -- classifiers target Python 3.12 +### Core Python Tooling -`[project.urls]` +- [pyproject.toml literal template](../assets/config-template/pyproject.toml) +- [.pre-commit-config.yaml literal template](../assets/config-template/.pre-commit-config.yaml) -- `Homepage = "https://github.com/TECHLETES/python_basic_template"` -- `Repository = "https://github.com/TECHLETES/python_basic_template"` -- `Issues = "https://github.com/TECHLETES/python_basic_template/issues"` +### GitHub Automation -### Dependencies +- [CI workflow template](../assets/config-template/.github/workflows/ci.yml) +- [Dependabot template](../assets/config-template/.github/dependabot.yml) -`[project].dependencies` +### Devcontainer -- `flask>=3.1.3,<4.0.0` -- `requests>=2.32.0,<3.0.0` -- `urllib3>=2.6.0,<3.0.0` -- `werkzeug>=3.1.6,<4.0.0` -- `filelock>=3.20.0,<4.0.0` +- [devcontainer.json template](../assets/config-template/.devcontainer/devcontainer.json) +- [Dockerfile template](../assets/config-template/.devcontainer/Dockerfile) +- [post-create.sh template](../assets/config-template/.devcontainer/post-create.sh) -`[dependency-groups].dev` +### VS Code Workspace -- `pre-commit>=4.5.0,<5.0.0` -- `detect-secrets>=1.5.0,<2.0.0` -- `nbstripout>=0.8.0,<1.0.0` -- `ipykernel>=6.29.0,<7.0.0` -- `pytest>=9.0.3,<10.0.0` -- `pytest-cov>=7.0.0,<8.0.0` -- `black>=25.12.0,<27.0.0` -- `mypy>=1.19.0,<2.0.0` -- `ruff>=0.14.0,<1.0.0` -- `beartype>=0.22.0,<1.0.0` -- `pytest-beartype>=0.2.0,<1.0.0` -- `pip-audit>=2.10.0,<3.0.0` -- `rich>=14.2.0,<15.0.0` -- `pyupgrade>=3.15.0,<4.0.0` -- `codespell>=2.2.0,<3.0.0` -- `bandit>=1.7.0,<2.0.0` -- `pydocstyle>=6.3.0,<7.0.0` -- `genbadge[coverage]>=1.1.0,<2.0.0` -- `testcontainers>=4.8.0` -- `pillow~=12.2` +- [extensions.json template](../assets/config-template/.vscode/extensions.json) +- [settings.json template](../assets/config-template/.vscode/settings.json) -`[dependency-groups].extra` +### Shell and Repo Hygiene -- present and currently empty except for a commented example +- [.envrc template](../assets/config-template/.envrc) +- [.detect-secrets.yaml template](../assets/config-template/.detect-secrets.yaml) +- [.gitattributes template](../assets/config-template/.gitattributes) +- [.gitignore template](../assets/config-template/.gitignore) -### setuptools package discovery +### Hook Scripts -`[tool.setuptools.packages.find]` - -- `where = ["."]` -- `include = ["utils*", "example*"]` -- `exclude = ["tests*"]` - -### Black - -`[tool.black]` - -- `line-length = 88` -- `target-version = ["py312"]` - -### pytest - -`[tool.pytest]` - -- `minversion = "9.0.3"` -- `addopts = [` - `"--strict-markers"`, - `"--strict-config"`, - `"--cov=utils"`, - `"--cov=example"`, - `"--cov-report=term-missing"`, - `"--cov-report=html"`, - `"--cov-report=xml"`, - `"--beartype-packages=utils,example"` - `]` -- `testpaths = ["tests"]` -- `python_files = ["test_*.py", "*_test.py"]` -- `python_classes = ["Test*"]` -- `python_functions = ["test_*"]` -- `pythonpath = ["."]` - -### coverage.py - -`[tool.coverage.run]` - -- `source = ["utils", "example"]` -- `omit = [` - `"*/__pycache__/*"`, - `"*/tests/*"`, - `"*/example/*"`, - `"*/.venv/*"`, - `"*/venv/*"`, - `"*/__init__.py"`, - `"setup.py"` - `]` - -`[tool.coverage.report]` - -- `exclude_lines = [` - `"pragma: no cover"`, - `"def __repr__"`, - `"if self.debug:"`, - `"if settings.DEBUG"`, - `"raise AssertionError"`, - `"raise NotImplementedError"`, - `"if 0:"`, - `"if __name__ == .__main__.:"`, - `"class .*\\bProtocol\\):"`, - `"@(abc\\.)?abstractmethod"`, - `"except ImportError"`, - `"pass"` - `]` - -### mypy - -`[tool.mypy]` - -- `python_version = "3.12"` -- `ignore_missing_imports = true` -- `follow_imports = "silent"` -- `warn_unused_ignores = true` -- `warn_return_any = true` -- `warn_unreachable = true` -- `no_implicit_optional = true` -- `allow_redefinition = true` -- `show_column_numbers = true` -- `pretty = true` -- `warn_unused_configs = true` -- `disallow_untyped_defs = true` -- `disallow_incomplete_defs = true` -- `files = ["example"]` -- `exclude = '''(?x)(^migrations/|^notebooks/|^tests/|^\.venv/|^venv/)'''` -- `plugins = []` - -### Ruff - -`[tool.ruff]` - -- `target-version = "py312"` -- `line-length = 88` -- `fix = true` -- `respect-gitignore = true` -- `extend-exclude = ["migrations"]` -- `lint.dummy-variable-rgx = "^_|^__|^pytest$"` - -`lint.select = ["E", "F", "I", "B", "UP"]` - -- `E` for pycodestyle -- `F` for pyflakes -- `I` for import sorting -- `B` for bugbear -- `UP` for pyupgrade - -`lint.ignore = ["B008", "E501"]` - -### Bandit - -`[tool.bandit]` - -- `exclude_dirs = ["tests", ".venv"]` -- `skips = ["B101", "B601"]` - -### pip-audit - -`[tool.pip-audit]` - -- `ignore = ["CVE-2026-1703"]` - -### uv - -`[tool.uv]` - -- `required-version = ">=0.9.25,<1.0.0"` - -## .pre-commit-config.yaml - -### Hook set and revisions - -- local `nbstripout-autoadd` - `entry: uv run scripts/hooks/nbstripout-autoadd.sh` - `language: system` - `types: [jupyter]` -- `psf/black` at `25.9.0` - `language_version: python3.12` - `types: [python]` -- `pre-commit/pre-commit-hooks` at `v6.0.0` - includes `trailing-whitespace`, `end-of-file-fixer`, `check-toml`, - `check-json`, `check-symlinks`, `check-yaml`, `check-added-large-files`, - `check-merge-conflict`, `detect-private-key`, `pretty-format-json` -- `end-of-file-fixer` excludes `coverage\.svg$` -- `pretty-format-json` uses `args: ["--autofix"]` and excludes `\.ipynb$` -- `pypa/pip-audit` at `v2.10.0` - `args: ["--progress-spinner", "off", "--desc"]` - `entry: uv run bash scripts/hooks/run-pip-audit.sh` - `language: system` - `stages: [pre-commit]` -- `Yelp/detect-secrets` at `v1.5.0` - `args: ["--baseline", ".secret.baseline"]` - `exclude: ^tests/` -- `mwouts/jupytext` at `v1.19.1` - `args: [--sync]` - `require_serial: true` -- `mirrors-mypy` at `v1.19.1` - `args: ["--config-file=pyproject.toml"]` - `pass_filenames: false` -- `asottile/pyupgrade` at `v3.21.2` - `args: [--py312-plus]` - `types: [python]` -- `astral-sh/ruff-pre-commit` at `v0.15.4` - `args: [--fix]` - `types: [python]` -- `PyCQA/bandit` at `1.9.4` - `args: [-ll, --skip, B101]` - `exclude: ^tests/` - `types: [python]` -- `PyCQA/pydocstyle` at `6.3.0` - `exclude: ^(tests/|example/)` - `types: [python]` -- local `pytest` - `entry: uv run pytest --cov --cov-fail-under=80 --beartype-packages=utils,example` - `language: system` - `pass_filenames: false` - `types: [python]` - `stages: [pre-commit]` - -## Devcontainer Configuration - -### .devcontainer/devcontainer.json - -- build context: `..` -- dockerfile: `Dockerfile` -- `containerEnv.PRE_COMMIT_HOME = "/home/vscode/.cache/pre-commit"` -- `containerEnv.UV_CACHE_DIR = "/home/vscode/.cache/uv"` -- `containerEnv.UV_LINK_MODE = "copy"` -- `containerEnv.UV_PYTHON_DOWNLOADS = "never"` -- VS Code extensions installed in container: - `ms-python.python`, `ms-python.vscode-pylance`, - `ms-python.mypy-type-checker`, `charliermarsh.ruff`, - `ms-toolsai.jupyter` -- VS Code settings in container: - `jupyter.jupyterServerType = "local"` - `python.defaultInterpreterPath = "${workspaceFolder}/.venv/bin/python"` - `python.terminal.activateEnvironment = true` - `python.testing.pytestArgs = ["."]` - `python.testing.pytestEnabled = true` - `python.testing.unittestEnabled = false` - `ruff.enable = true` -- mounts: - `source=python-template-uv-cache,target=/home/vscode/.cache/uv,type=volume` - `source=python-template-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume` -- `name = "python-basic-template"` -- `postCreateCommand = "bash .devcontainer/post-create.sh"` -- `remoteUser = "vscode"` -- `updateRemoteUserUID = true` -- `workspaceFolder = "/workspaces/${localWorkspaceFolderBasename}"` - -### .devcontainer/Dockerfile - -- base image: `mcr.microsoft.com/devcontainers/python:1-3.12-bookworm` -- `ARG USERNAME=vscode` -- `ARG UV_VERSION=0.9.25` -- installs `uv` into `/usr/local/bin` -- sets: - `PIP_DISABLE_PIP_VERSION_CHECK=1` - `PYTHONDONTWRITEBYTECODE=1` -- pre-creates cache directories for `uv` and `pre-commit` - -### .devcontainer/post-create.sh - -- changes to repo root -- prints `uv --version` and `python --version` -- runs `uv sync --frozen` -- runs `uv run pre-commit install --install-hooks` when `.git` exists -- checks `uv run python -c "import ipykernel"` -- reports whether `op` exists - -## GitHub Automation and Metadata - -### .github/workflows/ci.yml - -- workflow name: `CI` -- triggers: - pull requests to `main` and `staging` - pushes to `main` and `staging` -- `permissions.contents = write` - -`lint-and-test` job: - -- runs on `ubuntu-latest` -- `PRE_COMMIT_HOME = ${{ github.workspace }}/.pre-commit-cache` -- Python matrix: `['3.12']` -- uses: - `actions/checkout@v6` - `actions/setup-python@v6` - `astral-sh/setup-uv@v7` - `actions/cache@v4` - `stefanzweifel/git-auto-commit-action@v7` -- installs dependencies with `uv sync` -- caches pre-commit hooks using `.pre-commit-config.yaml` hash -- runs `uv run pre-commit run --all-files` -- runs `uv run pytest --cov --cov-report=term --cov-report=xml` -- on pull requests, generates `coverage.svg` with - `uv run genbadge coverage -i coverage.xml -o coverage.svg` -- on pull requests, auto-commits `coverage.svg` back to the PR branch with - commit message `chore: update coverage badge` - -`devcontainer-smoke` job: - -- runs on `ubuntu-latest` -- uses `actions/checkout@v6` -- uses `docker/setup-buildx-action@v3` -- uses `devcontainers/ci@v0.3` -- builds from `.devcontainer/devcontainer.json` -- verifies: - `.venv` exists - `.git/hooks/pre-commit` is executable - `uv --version` succeeds - `.venv/bin/python --version` succeeds - -### .github/dependabot.yml - -- `version: 2` -- update ecosystems: - `uv` in `/` daily, open PR limit `0` - `npm` in `/web` daily, open PR limit `0`, commit prefix `deps`, - `versioning-strategy: increase-if-necessary` - `docker` in `/` daily, open PR limit `0` - `github-actions` in `/` daily, open PR limit `0` - -### .github/pull_request_template.md - -- sections for: - description - type of change - contributor checklist - security considerations - testing - screenshots - related issues -- checklist explicitly asks for documentation updates, self-review, warning-free - changes, added tests, local test pass, and security review - -### .github/ISSUE_TEMPLATE/ - -`config.yml` - -- `blank_issues_enabled: false` -- contact links for: - discussions - private security advisory reporting - documentation - support/Q&A discussions - -`bug-report.md` - -- default title prefix: `[Bug] ` -- default label: `bug` -- collects reproduction steps, expected vs actual behavior, screenshots/logs, - environment, additional context, and acceptance criteria - -`feature-request.md` - -- default title prefix: `[FEATURE] ` -- default label: `enhancement` -- collects problem statement, solution, alternatives, use case, implementation - ideas, additional context, and priority/impact - -`docs_update.md` - -- default title prefix: `[DOCS] ` -- default label: `documentation` -- collects target location, issue type, current content, suggested changes, - context, audience, priority, related issues, and desired format - -## Workspace Editor Configuration - -### .vscode/extensions.json - -- recommended extensions: - `ms-vscode-remote.remote-containers` - `ms-azuretools.vscode-docker` - `ms-python.python` - `ms-python.vscode-pylance` - `ms-python.mypy-type-checker` - `charliermarsh.ruff` - `ms-toolsai.jupyter` - `astral-sh.ty` - `ms-python.autopep8` - `ms-python.black-formatter` - `ms-python.debugpy` - `mgesbert.python-path` - `docker.docker` - `dbaeumer.vscode-eslint` - `tamasfe.even-better-toml` - `cweijan.vscode-excel` - `GitHub.copilot-chat` - `github.vscode-github-actions` - `GitHub.vscode-pull-request-github` - `ms-azuretools.vscode-azure-github-copilot` - `mtxr.sqltools` - `mtxr.sqltools-driver-mysql` - `mtxr.sqltools-driver-pg` - `mtxr.sqltools-driver-sqlite` - `ms-ossdata.vscode-pgsql` - -### .vscode/settings.json - -- `mypy-type-checker.args = ["--config-file=pyproject.toml"]` -- `mypy-type-checker.cwd = "${workspaceFolder}"` -- `mypy-type-checker.importStrategy = "fromEnvironment"` -- `mypy-type-checker.reportingScope = "workspace"` -- `python.analysis.typeCheckingMode = "standard"` -- `python.defaultInterpreterPath = "${workspaceFolder}/.venv/bin/python"` -- `python.testing.pytestArgs = ["."]` -- `python.testing.pytestEnabled = true` -- `python.testing.unittestEnabled = false` -- `ruff.enable = true` - -## Shell Bootstrap and Repo Hygiene - -### .envrc - -- loads `.env` if present using `set -a` and `source .env` -- exports: - `PYTHONPATH=.` - `VENV_DIR=".venv"` - `PYPROJECT="pyproject.toml"` -- auto-activates `.venv/bin/activate` when `.venv` exists - -### .detect-secrets.yaml - -- excludes files matching `^tests/` - -### .gitignore - -- ignores Python caches and bytecode -- ignores virtual environments: `.venv/`, `venv/`, `env/`, `ENV/` -- ignores notebook checkpoints -- ignores mypy, Ruff, pytest, coverage, tox, nox caches and outputs -- ignores build and distribution artifacts -- ignores `.env` and `.env.*` -- ignores `.direnv/` - -### .gitattributes - -- `* text=auto` -- `*.sh eol=lf` -- `*.ps1 eol=crlf` -- `*.py eol=lf` -- `*.ipynb -text` - -## Scripts and Repo Workflows - -### scripts/setup.sh - -- shebang: `#!/usr/bin/env bash` -- strict mode: `set -euo pipefail` -- exports default env vars: - `VENV_DIR=.venv` - `PYPROJECT=pyproject.toml` -- verifies/install system prerequisites: Python 3, pip, git, uv, direnv, curl -- for `apt-get` hosts, installs Docker and Docker Compose if missing -- runs `uv lock` then `uv sync` -- configures locales to `en_US.UTF-8` -- appends `eval "$(direnv hook bash)"` to `~/.bashrc` when absent -- runs `direnv allow` if `.envrc` exists -- installs pre-commit hooks, runs `pre-commit autoupdate`, then - `pre-commit run --all-files` -- creates a secrets baseline with `detect-secrets scan > .secret.baseline` -- audits that baseline with `detect-secrets audit .secret.baseline` -- validates imports via `importlib.import_module("utils.secrets")` - -### scripts/new-branch.sh - -- usage: `./scripts/new-branch.sh ` -- supported types: `feature`, `fix`, `docs`, `ui`, `devops` -- lowercases and slugifies description into branch name -- branch format: `/` -- prunes `origin` remote-tracking branches -- deletes local branches already merged into `main`, excluding `main` -- creates the new branch from current HEAD with `git checkout -b` -- prints suggested follow-up commit and push commands - -### scripts/install-copilot-agents.sh - -- installs repo-shared Copilot assets into the user environment -- modes: `--copy` or `--link` -- replacement control: `--force` -- scope control: `--agents-only` or `--skills-only` -- env overrides: - `COPILOT_AGENTS_DIR` - `COPILOT_SKILLS_DIR` -- agents source: `.copilot/agents/*.agent.md` -- skills source: `.copilot/skills/*/SKILL.md` plus the full skill directory - -## Copilot Customization Surface - -### .github/copilot-instructions.md - -- states the repo is a TECHLETES Python template with `uv`, 1Password, - beartype plus mypy, pre-commit, and CI/CD expectations -- establishes four core rules: - secrets management - dependency management - mandatory type hints - committing from an active virtual environment -- documents daily commands and template-specific conventions for AI-assisted - coding - -### .copilot/ - -- contains versioned shared agents under `.copilot/agents/` -- contains versioned shared skills under `.copilot/skills/` -- install path is managed by `scripts/install-copilot-agents.sh` - -## Governance Files - -### CODE_OF_CONDUCT.md - -- repository includes a Contributor Covenant based code of conduct adapted for - TECHLETES internal collaboration +- [nbstripout-autoadd.sh template](../assets/config-template/scripts/hooks/nbstripout-autoadd.sh) +- [run-pip-audit.sh template](../assets/config-template/scripts/hooks/run-pip-audit.sh) ## How To Use During Audits -- Compare concrete values only where the target repo is expected to match the - template directly. -- Generalize package names, repo names, paths, coverage targets, and cache - identifiers to the target repo. -- If the target repo has an equivalent but intentionally different setting, - record that as either an approved deviation or an equivalence decision. +1. Start by comparing the target repo's controlling config files against these + literal templates. +2. Treat placeholders and comments in the asset files as required + generalization points, not deviations. +3. Record findings when the target repo diverges from the template's tooling, + workflow, or structure beyond those approved generalization points. +4. Fall back to the live repository files when an asset has not yet been added + for a config surface you need. From 57f26bf5b82d69195cc727270b7ee99093be088f Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Thu, 23 Apr 2026 18:12:15 +0200 Subject: [PATCH 132/199] feat: update development environment with additional VSCode extensions and pre-commit configuration --- .../.devcontainer/devcontainer.json | 23 ++++++++++++++++++- .../config-template/.pre-commit-config.yaml | 2 +- .../config-template/.vscode/extensions.json | 3 ++- .devcontainer/devcontainer.json | 23 ++++++++++++++++++- .pre-commit-config.yaml | 2 +- .vscode/extensions.json | 3 ++- 6 files changed, 50 insertions(+), 6 deletions(-) diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json index c0062762..00eb7364 100644 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json @@ -12,11 +12,32 @@ "customizations": { "vscode": { "extensions": [ + "ms-vscode-remote.remote-containers", + "ms-azuretools.vscode-docker", "ms-python.python", "ms-python.vscode-pylance", "ms-python.mypy-type-checker", "charliermarsh.ruff", - "ms-toolsai.jupyter" + "ms-toolsai.jupyter", + "astral-sh.ty", + "ms-python.autopep8", + "ms-python.black-formatter", + "ms-python.debugpy", + "mgesbert.python-path", + "docker.docker", + "dbaeumer.vscode-eslint", + "tamasfe.even-better-toml", + "cweijan.vscode-excel", + "GitHub.copilot-chat", + "github.vscode-github-actions", + "GitHub.vscode-pull-request-github", + "ms-azuretools.vscode-azure-github-copilot", + "mtxr.sqltools", + "mtxr.sqltools-driver-mysql", + "mtxr.sqltools-driver-pg", + "mtxr.sqltools-driver-sqlite", + "ms-ossdata.vscode-pgsql", + "formulahendry.docker-explorer" ], "settings": { "jupyter.jupyterServerType": "local", diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml b/.copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml index b8e93b9b..ba3f7224 100644 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: hooks: - id: mypy name: Type check with mypy - args: ["--config-file=pyproject.toml"] + args: [ "--config-file=pyproject.toml", "--install-types", "--non-interactive" ] pass_filenames: false # Modernize Python syntax diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json b/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json index 182171be..4355965e 100644 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json @@ -24,6 +24,7 @@ "mtxr.sqltools-driver-mysql", "mtxr.sqltools-driver-pg", "mtxr.sqltools-driver-sqlite", - "ms-ossdata.vscode-pgsql" + "ms-ossdata.vscode-pgsql", + "formulahendry.docker-explorer" ] } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b2fa5831..50deecb7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,11 +12,32 @@ "customizations": { "vscode": { "extensions": [ + "ms-vscode-remote.remote-containers", + "ms-azuretools.vscode-docker", "ms-python.python", "ms-python.vscode-pylance", "ms-python.mypy-type-checker", "charliermarsh.ruff", - "ms-toolsai.jupyter" + "ms-toolsai.jupyter", + "astral-sh.ty", + "ms-python.autopep8", + "ms-python.black-formatter", + "ms-python.debugpy", + "mgesbert.python-path", + "docker.docker", + "dbaeumer.vscode-eslint", + "tamasfe.even-better-toml", + "cweijan.vscode-excel", + "GitHub.copilot-chat", + "github.vscode-github-actions", + "GitHub.vscode-pull-request-github", + "ms-azuretools.vscode-azure-github-copilot", + "mtxr.sqltools", + "mtxr.sqltools-driver-mysql", + "mtxr.sqltools-driver-pg", + "mtxr.sqltools-driver-sqlite", + "ms-ossdata.vscode-pgsql", + "formulahendry.docker-explorer" ], "settings": { "jupyter.jupyterServerType": "local", diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 44e456bf..edb1228d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: hooks: - id: mypy name: Type check with mypy - args: [ "--config-file=pyproject.toml" ] + args: [ "--config-file=pyproject.toml", "--install-types", "--non-interactive" ] pass_filenames: false # Modernize Python syntax diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 182171be..4355965e 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -24,6 +24,7 @@ "mtxr.sqltools-driver-mysql", "mtxr.sqltools-driver-pg", "mtxr.sqltools-driver-sqlite", - "ms-ossdata.vscode-pgsql" + "ms-ossdata.vscode-pgsql", + "formulahendry.docker-explorer" ] } From f13e2d16c228593b9f57301a11df490623de5fd7 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Thu, 30 Apr 2026 14:40:11 +0000 Subject: [PATCH 133/199] feat: enhance audit skill documentation and Dockerfile with new config handling and dependencies --- .../audit-python-template-compliance/SKILL.md | 21 ++++++++---- .../config-template/.devcontainer/Dockerfile | 7 ++++ .../references/audit-procedure.md | 17 +++++++--- .../references/template-baseline.md | 13 +++---- .../references/template-config-settings.md | 34 +++++++++++-------- .devcontainer/Dockerfile | 7 ++++ pyproject.toml | 2 +- 7 files changed, 68 insertions(+), 33 deletions(-) diff --git a/.copilot/skills/audit-python-template-compliance/SKILL.md b/.copilot/skills/audit-python-template-compliance/SKILL.md index dc074aea..1a59d308 100644 --- a/.copilot/skills/audit-python-template-compliance/SKILL.md +++ b/.copilot/skills/audit-python-template-compliance/SKILL.md @@ -1,6 +1,6 @@ --- name: audit-python-template-compliance -description: 'Audit a Python repository against the TECHLETES python_basic_template and bring it into compliance. Use when you need to compare a repo to the template, review drift in pyproject/pre-commit/CI/devcontainer/docs/secrets workflow, remediate gaps, rerun the audit, validate the repo, and produce a compliance report.' +description: 'Audit a Python repository against the TECHLETES python_basic_template and bring it into compliance. Use when you need to align a repo to the template by copying the shipped config bases, review drift in pyproject/pre-commit/CI/devcontainer/docs/secrets workflow, remediate gaps, rerun the audit, validate the repo, and produce a compliance report.' argument-hint: 'Target repository path or repo name, plus any approved exceptions from the template' user-invocable: true --- @@ -31,18 +31,22 @@ available validation commands, and finish with a concise audit report. 1. Confirm the target repository and record any approved exceptions before changing files. 2. Treat executable configuration as the source of truth when prose docs drift. -3. Generalize template-specific values instead of copying them literally. - Package names, repo URLs, excluded directories, cache volume names, and - secret references must match the target repository. -4. Fix root causes first. Do not patch around validation failures with +3. For config-bearing files shipped with this skill, copy the provided file as + the base instead of recreating it. Prefer a matching literal file from this + skill directory first, then from `assets/config-template/`, and only after + that replace the approved target-specific values. +4. Package names, repo URLs, excluded directories, cache volume names, and + secret references must match the target repository. +5. Fix root causes first. Do not patch around validation failures with one-off exceptions unless the user explicitly approves the deviation. ## Required References - Read the template baseline in [template-baseline](./references/template-baseline.md). - Use the asset index in [template-config-settings](./references/template-config-settings.md) - and its linked literal templates when the audit needs exact tool settings - instead of capability-level checks. + as the default copy source for config files. When a matching shipped file + exists, copy it first and then make only the approved target-specific + substitutions. - Follow the execution sequence in [audit-procedure](./references/audit-procedure.md). - Use the final structure in [audit-report-template](./assets/audit-report-template.md). @@ -64,6 +68,9 @@ By the end of the run, produce all of the following when feasible: `.pre-commit-config.yaml`, `.github/workflows/`, `.devcontainer/`, `scripts/hooks/`, `README.md`, `docs/`, first-party packages, and test entry points. +- When a config surface has a shipped base file in this skill, copy that file + into place before making local adjustments. Do not rebuild those configs from + memory. - Keep the first edits small and validate immediately. - Parallelize only independent slices, such as docs versus CI copy edits, after you know they do not share the same controlling config. diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile index 404bfc23..61284dcc 100644 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile @@ -8,6 +8,13 @@ ADD https://astral.sh/uv/${UV_VERSION}/install.sh /tmp/uv-installer.sh RUN env UV_UNMANAGED_INSTALL="/usr/local/bin" sh /tmp/uv-installer.sh \ && rm /tmp/uv-installer.sh +RUN rm -f /etc/apt/sources.list.d/yarn.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + bubblewrap \ + socat \ + && rm -rf /var/lib/apt/lists/* + ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ PYTHONDONTWRITEBYTECODE=1 diff --git a/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md b/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md index 94e512e9..ce82ead0 100644 --- a/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md +++ b/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md @@ -55,10 +55,11 @@ Primary evidence set: `scripts/hooks/`, and audit wrappers - Copilot assets under `.copilot/` when present -Use `template-config-settings.md` and its linked literal asset files to compare -concrete values for tool settings such as pytest `addopts`, mypy strictness +Use `template-config-settings.md` and its shipped config files as the default +base for concrete tool settings such as pytest `addopts`, mypy strictness flags, Ruff rule sets, Bandit skips, pre-commit hook revisions, delegated hook -scripts, and devcontainer editor settings. +scripts, and devcontainer editor settings. When a matching shipped file exists, +copy it first and then replace only the approved target-specific values. If a baseline file is missing, treat that as a finding rather than continuing as though the capability exists. @@ -121,12 +122,18 @@ baseline. Rules: -1. Generalize template details to the target repo. +1. For shipped config surfaces, copy the matching file from this skill as the + base before editing. Prefer files shipped directly with the skill, then + `assets/config-template/`. 2. Prefer adding missing baseline capabilities over documenting gaps away. 3. If the target repo already has an equivalent mechanism, align and document it instead of replacing it unnecessarily. 4. Keep edits reversible and validate the touched slice immediately. +For missing or substantially divergent config files, do not rebuild them from +memory. Start from the shipped base file, then apply target-specific +substitutions. + ### Focused Validation After Each Edit Slice After the first substantive edit, run the cheapest meaningful check before doing @@ -203,6 +210,6 @@ Use these rules when the target repo does not match the template cleanly. then document the equivalence. - Docs contradict automation: trust the executable config first. - Target repo contains stale template placeholders: replace them with target repo - values rather than copying more template text. + values after copying the shipped base file rather than recreating the config. - Validation unavailable due to environment: report it explicitly, with the exact command that could not be run. diff --git a/.copilot/skills/audit-python-template-compliance/references/template-baseline.md b/.copilot/skills/audit-python-template-compliance/references/template-baseline.md index 18aef019..7e88f219 100644 --- a/.copilot/skills/audit-python-template-compliance/references/template-baseline.md +++ b/.copilot/skills/audit-python-template-compliance/references/template-baseline.md @@ -17,9 +17,9 @@ When template sources disagree, prefer them in this order: Do not copy stale prose over working automation. Align the docs to the actual behavior unless the automation is clearly outdated and should be changed. -When you need exact comparison values for tool configuration, use -`template-config-settings.md` and the linked literal asset templates alongside -this baseline. +When you need exact config values for tool configuration, use +`template-config-settings.md` and the shipped asset files as the base, then +apply only the allowed target-specific substitutions. ## Baseline Areas @@ -125,16 +125,17 @@ this baseline. - If the repo ships Copilot customizations, the repo should keep those assets versioned and installable from the repository. -## Generalize, Do Not Copy Literally +## Copy As Base, Then Generalize Target Values -Some template values are examples and must be rewritten for the target repo. +For shipped config surfaces, copy the provided base file first. Only the +target-specific values below should be rewritten for the target repo. | Template surface | Template example | Generalize for target repo | | --- | --- | --- | | First-party packages | `utils`, `example` | Use the target repo's actual importable packages across setuptools, pytest, coverage, mypy, beartype, and smoke tests | | Project URLs | `TECHLETES/python_basic_template` | Point to the target repository's actual homepage, repo, and issues URLs | | Devcontainer names | `python-basic-template` | Rename the container and cache volumes for the target repository | -| Excluded directories | `migrations`, `notebooks`, `.venv`, `tests` | Exclude the target repo's generated, vendored, or non-source directories instead of copying the template list blindly | +| Excluded directories | `migrations`, `notebooks`, `.venv`, `tests` | Adjust the copied list only where the target repo's generated, vendored, or non-source directories differ | | Coverage scope | `utils`, `example` | Measure the target repo's production packages, not examples or template names | | Secret references | `op://...` examples | Use the target team's real vault/item/field references | | Example dependencies | Flask, requests, etc. | Keep only packages the target repo actually uses | diff --git a/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md b/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md index ac016e87..a4372ce0 100644 --- a/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md +++ b/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md @@ -1,16 +1,19 @@ # Template Config Assets -This reference now acts as an index to generalized literal config files stored -under `assets/config-template/`. +This reference acts as the copy-first index for config files shipped with this +skill. -Use these asset files when you need to compare a target repository against the -template's actual config shape, syntax, comments, and default settings. +When a target repository needs one of these config surfaces, copy the matching +shipped file as the base instead of recreating the file by hand. Prefer a +matching literal file shipped directly in this skill directory when one exists; +otherwise copy the file from `assets/config-template/`. After copying, replace +only the approved target-specific values. ## Generalization Rules The asset files intentionally preserve the template's literal file structure, -formatting, and most current settings. Only these classes of values are -generalized with comments or placeholders: +formatting, comments, and current settings. Only these classes of values should +be generalized after the copy step: - runtime dependencies and optional production dependencies - target-repository identity values such as repo URLs or project names @@ -21,6 +24,9 @@ generalized with comments or placeholders: If an asset file and the current repository diverge, the repository file is the source of truth and the asset should be updated. +Do not synthesize these files from memory or rewrite them section-by-section +when a shipped base file exists. + ## Asset Files ### Core Python Tooling @@ -58,11 +64,11 @@ source of truth and the asset should be updated. ## How To Use During Audits -1. Start by comparing the target repo's controlling config files against these - literal templates. -2. Treat placeholders and comments in the asset files as required - generalization points, not deviations. -3. Record findings when the target repo diverges from the template's tooling, - workflow, or structure beyond those approved generalization points. -4. Fall back to the live repository files when an asset has not yet been added - for a config surface you need. +1. Identify whether the needed config file is shipped directly with this skill + or under `assets/config-template/`, then copy that file as the base. +2. Replace only the approved target-specific values and placeholders after the + copy step. +3. Record findings when the target repo still diverges from the template's + tooling, workflow, or structure beyond those approved generalization points. +4. Fall back to the live repository files only when this skill does not yet + ship a base file for the config surface you need. diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 404bfc23..61284dcc 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -8,6 +8,13 @@ ADD https://astral.sh/uv/${UV_VERSION}/install.sh /tmp/uv-installer.sh RUN env UV_UNMANAGED_INSTALL="/usr/local/bin" sh /tmp/uv-installer.sh \ && rm /tmp/uv-installer.sh +RUN rm -f /etc/apt/sources.list.d/yarn.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + bubblewrap \ + socat \ + && rm -rf /var/lib/apt/lists/* + ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ PYTHONDONTWRITEBYTECODE=1 diff --git a/pyproject.toml b/pyproject.toml index cde02faa..5eca8842 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -194,7 +194,7 @@ skips = ["B101", "B601"] # Used by both pre-commit hooks and CI workflows via scripts/hooks/run-pip-audit.sh # See docs/6_pre_commit_troubleshooting.md for explanation of each exclusion ignore = [ - "CVE-2026-1703", # Path traversal in pip when extracting malicious wheels + "CVE-2026-3219" # pip 26.0.1 pip handles concatenated tar and ZIP files as ZIP files regardless of filename or whether a file is both a tar and ZIP file. This behavior could result in confusing installation behavior, such as installing "incorrect" files according to the filename of the archive. New behavior only proceeds with installation if the file identifies uniquely as a ZIP or tar archive, not as both. ] [tool.uv] From 318872bfc65ccaf6445d34b0badcb749e9f140ef Mon Sep 17 00:00:00 2001 From: MaartenHofland Date: Wed, 6 May 2026 12:01:21 +0200 Subject: [PATCH 134/199] =?UTF-8?q?feat:=20update=20install=20script=20to?= =?UTF-8?q?=20include=20no-force=20option=20and=20set=20defaul=E2=80=A6=20?= =?UTF-8?q?(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: update install script to include no-force option and set default behavior to replace existing files * fix: correct formatting in documentation for JavaScript/Bun vulnerability resolution --- .../SKILL.md | 136 ++---------------- scripts/install-copilot-agents.sh | 10 +- 2 files changed, 23 insertions(+), 123 deletions(-) diff --git a/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md b/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md index 1bc2ae58..29be3d74 100644 --- a/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md +++ b/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md @@ -191,17 +191,6 @@ If `uv lock` fails with version conflicts: package-b = ">=3.0,<4.0" ``` -### Step 1g: Commit Python Changes - -```bash -git add pyproject.toml uv.lock -git commit -m "security: update Python dependencies - fix CVEs in pypdf, langsmith, langchain-text-splitters" -``` - -Use commit message format: `security: ` - ---- - ## Phase 1 Validation: Python Tests Once dependencies are locked and synced: @@ -336,39 +325,9 @@ Once bun.lock is updated and all packages installed: - Consider a more conservative version constraint - Review package changelogs for migration guides -### Step 2g: Commit JavaScript Changes - -```bash -git add package.json bun.lock -git commit -m "security: update JavaScript dependencies - fix CVEs in frontend packages" -``` - -Use commit message format: `security: ` - --- -## Phase 3: Pull Request and Summary - -### Step 3a: Push Branch to Remote - -```bash -git push origin security-updates-dependencies -``` - -### Step 3b: Create Pull Request with Detailed Summary - -Create a PR from `security-updates-dependencies` to the base branch (staging/main/master) with a comprehensive description. - -#### PR Title Format -``` -security: fix vulnerability updates in Python and JavaScript dependencies -``` -**Note**: If major version bumps or breaking changes were required, prefix the title with `[breaking]`: -`security: [breaking] fix critical vulnerability updates with breaking changes` -#### PR Description Template - -```markdown ## Security Update Summary ### Major / Breaking Changes @@ -463,30 +422,6 @@ security: fix vulnerability updates in Python and JavaScript dependencies - [x] Tests pass (Python) - [x] Build pipeline verified (JavaScript) - [x] Lock files updated and committed -- [x] Comprehensive PR description provided -``` - -### Step 3c: Create Pull Request - -Use available agent tools or the GitHub CLI to create the PR. Avoid using manual web interfaces. - -**Method 1: Using Agent Tools (Preferred)** -If the `create-pull-request` skill or `github-pull-request_create_pull_request` tool is available, use it directly with the summary generated in Step 3b. - -**Method 2: Using GitHub CLI** -If agent tools are unavailable but the `gh` CLI is installed: -```bash -# Save summary to a temp file -echo "SUMMARY_FROM_STEP_3B" > /tmp/pr-body.md - -# Create PR -gh pr create \ - --title "security: fix vulnerability updates in Python and JavaScript dependencies" \ - --body-file /tmp/pr-body.md \ - --base $BASE_BRANCH \ - --head security-updates-dependencies \ - --label "security,dependencies" -``` --- @@ -498,22 +433,11 @@ gh pr create \ โœ“ `uv lock` generates successfully with no version conflicts โœ“ `uv sync` completes without errors โœ“ `pytest` passes all tests -โœ“ Ignored CVEs in `[tool.pip-audit]` are reviewed and documented in PR +โœ“ Ignored CVEs in `[tool.pip-audit]` are reviewed and documented โœ“ JavaScript/bun vulnerabilities are scanned and analyzed โœ“ All CRITICAL/HIGH JavaScript vulnerabilities checked (resolved or documented) โœ“ `bun run dev` starts without errors โœ“ Frontend tests pass (if applicable) -โœ“ Python changes committed to branch -โœ“ JavaScript changes committed to branch (if applicable) -โœ“ Branch pushed to remote (`git push origin security-updates-dependencies`) -โœ“ Pull Request created with: - - โœ“ Detailed vulnerability summary (Python & JavaScript) - - โœ“ Explanation of what was fixed and why - - โœ“ Documentation of vulnerabilities NOT fixed and reasoning - - โœ“ Validation results included - - โœ“ Files changed listed - - โœ“ Appropriate labels (security, dependencies) -โœ“ PR assigned to appropriate reviewers --- @@ -522,36 +446,22 @@ gh pr create \ **For Git Workflow**: - Base branch: Use staging if it exists, otherwise main, otherwise master - Branch creation: Always create `security-updates-dependencies` branch -- PR creation: Always create PR after all work is done -- Never force-push to base branches **For Python (pip audit):** - Stop when: All CRITICAL and HIGH CVEs are fixed, and all tests pass -- Exception: Document and justify any LOW severity CVEs left unfixed in PR description -- Never commit unresolved CRITICAL vulnerabilities -- Document reasoning in PR for any vulnerabilities that cannot be fixed -- **Breaking Changes**: If a fix requires a major version bump or breaking change, ensure the PR title starts with `[breaking]` and the `Major / Breaking Changes` section of the description clearly lists these updates. +- Exception: Document and justify any LOW severity CVEs left unfixed +- Document reasoning for any vulnerabilities that cannot be fixed **For JavaScript/Bun:** - Stop when: All CRITICAL and HIGH vulnerabilities are analyzed -- If resolved: Great! Document in PR +- If resolved: Great! - If not resolved: Document thoroughly WHY they remain (transitive deps, upstream waiting, etc.) - Never commit unresolved CRITICAL vulnerabilities without clear mitigation path -- Document transitive/dev-only vulnerabilities in PR for transparency +- Document transitive/dev-only vulnerabilities for transparency **Version Conflicts:** - Iterate max 3-5 times; if no solution, escalate to maintainers - Consider alternative packages if maintainer is unresponsive -- Document all conflict resolution attempts in commit messages - -**PR Creation Requirements**: -- Title: Use `security: ` format -- Description: Use comprehensive template covering all findings -- Method: Use available agent tools (`create-pull-request`, `github-pull-request_create_pull_request`) or GitHub CLI -- Base branch: Use detected base branch (staging/main/master) -- Head branch: Always `security-updates-dependencies` -- Labels: Always add `security` and `dependencies` labels -- Reviewers: Assign to security or ops team if applicable --- @@ -566,16 +476,11 @@ gh pr create \ | Lock Python deps | `uv lock` | | Install Python deps | `uv sync` | | Run Python tests | `uv run pytest` | -| Commit Python changes | `git add pyproject.toml uv.lock && git commit -m "security: ..."` | | Scan JavaScript | `bun audit` | | Update JS deps | `bun update` โ†’ `bun install` | | Run frontend | `bun run dev` | | Run JS tests | `bun run test` | -| Commit JS changes | `git add package.json bun.lock && git commit -m "security: ..."` | -| Push branch | `git push origin security-updates-dependencies` | -| Create PR (CLI) | `gh pr create --title "..." --body "..." --base ` | | View branch status | `git status` | -| View commits | `git log --oneline -5` | --- @@ -584,10 +489,8 @@ gh pr create \ โŒ **Don't** work directly on staging/main/master branches; always use feature branch โŒ **Don't** forget to pull latest before creating feature branch โŒ **Don't** force-push the feature branch without good reason -โŒ **Don't** skip the PR; commit and push directly to main is anti-pattern -โŒ **Don't** create PR with generic description; use comprehensive template +โŒ **Don't** create commits & PR; commits and PR are done by user, to keep human in the loop โŒ **Don't** forget to document why vulnerabilities were NOT fixed -โŒ **Don't** merge PRs without security review โŒ **Don't** update everything at once; update prioritized vulnerabilities โŒ **Don't** skip tests; always validate after dependency updates โŒ **Don't** ignore CRITICAL vulnerabilities; fix them or mitigate with security controls @@ -602,33 +505,24 @@ gh pr create \ ## Success Criteria A successful vulnerability remediation: -1. **Git workflow completed**: Feature branch created, all changes committed, branch pushed to remote +1. **Git workflow completed**: Feature branch created and up to date 2. **All CRITICAL vulnerabilities are fixed** 3. **All HIGH vulnerabilities are fixed or well-documented** 4. **All code tests pass** (Python + JavaScript, if applicable) 5. **All applications start without errors** 6. **Dependency versions are locked in version control** (`uv.lock`, `bun.lock`) -7. **Pull Request created** with: - - Clear title describing the security updates - - Comprehensive description covering: - - What vulnerabilities were fixed (with CVE IDs and versions) - - Why vulnerabilities were NOT fixed (if any) - - Validation results and test outcomes - - Files changed - - Appropriate labels (`security`, `dependencies`) - - Assigned to relevant reviewers 8. **Documentation complete**: All decisions, conflicts, and resolutions explained --- ## Example Prompts to Use This Skill -- "Fix all vulnerabilities in the Python dependencies and create a PR" +- "Fix all vulnerabilities in the Python dependencies" - "Run security audits on all dependencies and update critical CVEs" -- "Scan for CVEs, fix what we can, and open a PR with detailed findings" -- "Update dependencies to fix security vulnerabilities with automated branch and PR" -- "Clean up old ignored CVEs and update them if fixes exist, then create PR" -- "Security audit: scan Python and JavaScript, fix vulnerabilities, create PR to staging" -- "Create feature branch for security updates, fix dependencies, and open pull request" -- "Resolve the version conflict between package-a and package-b and create security PR" -- "Validate that the app still works after security updates and propose PR" +- "Scan for CVEs, fix what we can" +- "Update dependencies to fix security vulnerabilities with automated branch" +- "Clean up old ignored CVEs and update them if fixes exist" +- "Security audit: scan Python and JavaScript, fix vulnerabilities" +- "Create feature branch for security updates, fix dependencies" +- "Resolve the version conflict between package-a and package-b" +- "Validate that the app still works after security updates" diff --git a/scripts/install-copilot-agents.sh b/scripts/install-copilot-agents.sh index b067b975..d4a130a3 100755 --- a/scripts/install-copilot-agents.sh +++ b/scripts/install-copilot-agents.sh @@ -3,7 +3,7 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" mode="copy" -force="false" +force="true" install_agents="true" install_skills="true" @@ -12,12 +12,14 @@ usage() { Install Copilot agent definitions and skills from this repository. Usage: - ./scripts/install-copilot-agents.sh [--copy|--link] [--force] [--agents-only|--skills-only] + ./scripts/install-copilot-agents.sh [--copy|--link] [--no-force] [--agents-only|--skills-only] Options: --copy Copy the files into the target directory (default). --link Symlink the files into the target directory. --force Replace existing files with the repository versions. + This is the default behavior. + --no-force Skip existing files instead of replacing them. --agents-only Install only agent definitions (skip skills). --skills-only Install only skills (skip agents). --help Show this help message. @@ -29,6 +31,7 @@ Environment: Examples: ./scripts/install-copilot-agents.sh ./scripts/install-copilot-agents.sh --link + ./scripts/install-copilot-agents.sh --no-force ./scripts/install-copilot-agents.sh --skills-only COPILOT_AGENTS_DIR="$HOME/.copilot/agents" ./scripts/install-copilot-agents.sh --force EOF @@ -45,6 +48,9 @@ while [[ $# -gt 0 ]]; do --force) force="true" ;; + --no-force) + force="false" + ;; --agents-only) install_skills="false" ;; From 75ef6ebddf11759781b219582e8eaebbf74def29 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 6 May 2026 12:01:59 +0200 Subject: [PATCH 135/199] Add CodeQL remediation templates and workflow documentation (#30) - Introduced a comprehensive CodeQL remediation report template to summarize analysis findings and remediation efforts. - Created a task delegation template for assigning CodeQL fixes to coder agents, outlining objectives, implementation constraints, and verification steps. - Documented a step-by-step CodeQL workflow for running analysis and orchestrating remediation, including database management and finding prioritization. - Established safe remediation rules to guide developers in fixing CodeQL findings effectively and securely. - Added SARIF interpretation guidelines to assist in understanding and classifying CodeQL findings. - Compiled language-specific verification commands to ensure repository integrity post-remediation. --- .../codeql-analysis-remediation/SKILL.md | 93 ++++++ .../assets/codeql-commands.sh | 221 +++++++++++++ .../assets/report-template.md | 220 +++++++++++++ .../assets/task-delegate-template.md | 145 +++++++++ .../references/codeql-workflow.md | 230 +++++++++++++ .../references/safe-remediation-rules.md | 305 ++++++++++++++++++ .../references/sarif-interpretation.md | 154 +++++++++ .../references/verification-commands.md | 182 +++++++++++ .devcontainer/devcontainer.json | 3 +- .devcontainer/post-create.sh | 21 ++ pyproject.toml | 3 +- 11 files changed, 1575 insertions(+), 2 deletions(-) create mode 100644 .copilot/skills/codeql-analysis-remediation/SKILL.md create mode 100644 .copilot/skills/codeql-analysis-remediation/assets/codeql-commands.sh create mode 100644 .copilot/skills/codeql-analysis-remediation/assets/report-template.md create mode 100644 .copilot/skills/codeql-analysis-remediation/assets/task-delegate-template.md create mode 100644 .copilot/skills/codeql-analysis-remediation/references/codeql-workflow.md create mode 100644 .copilot/skills/codeql-analysis-remediation/references/safe-remediation-rules.md create mode 100644 .copilot/skills/codeql-analysis-remediation/references/sarif-interpretation.md create mode 100644 .copilot/skills/codeql-analysis-remediation/references/verification-commands.md diff --git a/.copilot/skills/codeql-analysis-remediation/SKILL.md b/.copilot/skills/codeql-analysis-remediation/SKILL.md new file mode 100644 index 00000000..d4e0968b --- /dev/null +++ b/.copilot/skills/codeql-analysis-remediation/SKILL.md @@ -0,0 +1,93 @@ +# Skill: CodeQL repository analysis and remediation orchestration + +## Purpose + +Use this skill when the task is to analyze the current repository with CodeQL, identify security or quality issues, plan safe fixes, delegate implementation work to parallel coder agents, and verify that the repository still works after changes. + +The goal is not only to run CodeQL, but to turn the findings into controlled, reviewable code improvements. + +## Operating mode + +You are the orchestration agent. Your default workflow is to: + +1. Inspect the repository and determine applicable languages +2. Check CodeQL CLI availability +3. Create or reuse CodeQL databases +4. Run CodeQL analysis +5. Interpret findings and create an implementation plan +6. Delegate independent tasks to parallel coder agents +7. Review changes and run verification checks +8. Produce a final remediation report + +**Do not immediately edit code.** Delegate all but the smallest changes to coder agents. + +## Quick start + +See [references/codeql-workflow.md](references/codeql-workflow.md) for the detailed step-by-step workflow. + +Key steps: +- **Check tools:** `codeql version` +- **Supported languages:** Python (`python`), JavaScript/TypeScript (`javascript-typescript`) +- **Database directory:** `.codeql/dbs/` (reuse if possible) +- **Results directory:** `.codeql/results/` (SARIF format) +- **Finding prioritization:** See [references/sarif-interpretation.md](references/sarif-interpretation.md) +- **Safe remediation rules:** See [references/safe-remediation-rules.md](references/safe-remediation-rules.md) + +## Key constraints + +- Do not recreate CodeQL databases unnecessarily; reuse when safe +- Only run CodeQL for languages actually present in the repository +- Do not blindly fix every CodeQL finding; classify each finding first +- Do not suppress findings without clear justification +- Do not make broad refactors while fixing security findings +- Always verify that the repository still works after changes + +## Repository inspection checklist + +**Python indicators:** +- `*.py` files, `pyproject.toml`, `requirements.txt`, `setup.py`, `setup.cfg`, `Pipfile`, `uv.lock`, `poetry.lock` + +**JavaScript/TypeScript indicators:** +- `*.js`, `*.jsx`, `*.ts`, `*.tsx` files, `package.json`, `tsconfig.json`, `vite.config.*`, `next.config.*`, `webpack.config.*` + +## Delegation to coder agents + +Use [assets/task-delegate-template.md](assets/task-delegate-template.md) as a template. + +Each task must include: +- Objective and CodeQL context +- Scope (files allowed to modify, do not modify) +- Implementation constraints +- Verification commands +- Expected output + +Assign independent tasks to parallel coder agents. Avoid assigning multiple agents to edit the same file. + +## Review and verification + +After coder agents return changes: +1. Inspect all diffs +2. Check whether each task addresses the finding +3. Check for regressions or unrelated changes +4. Use language-specific verification commands (see [references/verification-commands.md](references/verification-commands.md)) +5. Rerun CodeQL for affected languages if changes are substantial + +## Final report + +Use [assets/report-template.md](assets/report-template.md) as a template. + +A fix is complete only when: +- The targeted finding is gone, **or** +- The finding is proven to be a false positive and documented, **or** +- The remaining risk requires explicit human product or security decision + +--- + +**Related files:** +- [references/codeql-workflow.md](references/codeql-workflow.md) โ€” Detailed step-by-step workflow +- [references/sarif-interpretation.md](references/sarif-interpretation.md) โ€” How to interpret and prioritize findings +- [references/safe-remediation-rules.md](references/safe-remediation-rules.md) โ€” Safety guidelines for fixing issues +- [references/verification-commands.md](references/verification-commands.md) โ€” Language-specific verification +- [assets/codeql-commands.sh](assets/codeql-commands.sh) โ€” Reusable CodeQL shell commands +- [assets/task-delegate-template.md](assets/task-delegate-template.md) โ€” Template for delegating tasks +- [assets/report-template.md](assets/report-template.md) โ€” Final report template diff --git a/.copilot/skills/codeql-analysis-remediation/assets/codeql-commands.sh b/.copilot/skills/codeql-analysis-remediation/assets/codeql-commands.sh new file mode 100644 index 00000000..c169d0e9 --- /dev/null +++ b/.copilot/skills/codeql-analysis-remediation/assets/codeql-commands.sh @@ -0,0 +1,221 @@ +#!/bin/bash +# CodeQL command reference +# Reusable CodeQL commands for database creation, analysis, and reanalysis + +set -e + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +# Check if CodeQL CLI is available +check_codeql() { + if ! command -v codeql &> /dev/null; then + echo "Error: CodeQL CLI not found. Install CodeQL and ensure it is on PATH." + return 1 + fi + echo "โœ“ CodeQL CLI found: $(codeql version | head -1)" +} + +# Create base directories for CodeQL +init_codeql_dirs() { + mkdir -p .codeql/dbs + mkdir -p .codeql/results + echo "โœ“ CodeQL directories initialized" +} + +# ============================================================================ +# DATABASE OPERATIONS +# ============================================================================ + +# Create Python database +create_python_db() { + echo "Creating Python CodeQL database..." + if [ -d ".codeql/dbs/python" ]; then + echo "โš  Python database already exists at .codeql/dbs/python" + return 0 + fi + init_codeql_dirs + codeql database create .codeql/dbs/python \ + --language=python \ + --source-root=. + echo "โœ“ Python database created at .codeql/dbs/python" +} + +# Create JavaScript/TypeScript database +create_js_ts_db() { + echo "Creating JavaScript/TypeScript CodeQL database..." + if [ -d ".codeql/dbs/javascript-typescript" ]; then + echo "โš  JavaScript/TypeScript database already exists" + return 0 + fi + init_codeql_dirs + codeql database create .codeql/dbs/javascript-typescript \ + --language=javascript-typescript \ + --source-root=. + echo "โœ“ JavaScript/TypeScript database created" +} + +# Validate a database +validate_db() { + local db_path="$1" + if [ -z "$db_path" ]; then + echo "Usage: validate_db " + return 1 + fi + if [ ! -d "$db_path" ]; then + echo "Error: Database not found at $db_path" + return 1 + fi + codeql database print-baseline "$db_path" + echo "โœ“ Database validated: $db_path" +} + +# ============================================================================ +# ANALYSIS OPERATIONS +# ============================================================================ + +# Analyze Python (security and quality) +analyze_python_default() { + echo "Analyzing Python repository..." + if [ ! -d ".codeql/dbs/python" ]; then + echo "Error: Python database not found. Create it first with create_python_db" + return 1 + fi + init_codeql_dirs + codeql database analyze .codeql/dbs/python \ + codeql/python-queries:codeql-suites/python-security-and-quality.qls \ + --download \ + --format=sarif-latest \ + --output=.codeql/results/codeql-python.sarif + echo "โœ“ Python analysis complete: .codeql/results/codeql-python.sarif" +} + +# Analyze Python (security only, less noise) +analyze_python_security() { + echo "Analyzing Python repository (security only)..." + if [ ! -d ".codeql/dbs/python" ]; then + echo "Error: Python database not found. Create it first with create_python_db" + return 1 + fi + init_codeql_dirs + codeql database analyze .codeql/dbs/python \ + codeql/python-queries:codeql-suites/python-security-extended.qls \ + --download \ + --format=sarif-latest \ + --output=.codeql/results/codeql-python-security.sarif + echo "โœ“ Python security analysis complete" +} + +# Analyze JavaScript/TypeScript (security and quality) +analyze_js_ts_default() { + echo "Analyzing JavaScript/TypeScript repository..." + if [ ! -d ".codeql/dbs/javascript-typescript" ]; then + echo "Error: JS/TS database not found. Create it first with create_js_ts_db" + return 1 + fi + init_codeql_dirs + codeql database analyze .codeql/dbs/javascript-typescript \ + codeql/javascript-queries:codeql-suites/javascript-security-and-quality.qls \ + --download \ + --format=sarif-latest \ + --output=.codeql/results/codeql-javascript-typescript.sarif + echo "โœ“ JavaScript/TypeScript analysis complete" +} + +# Analyze JavaScript/TypeScript (security only, less noise) +analyze_js_ts_security() { + echo "Analyzing JavaScript/TypeScript repository (security only)..." + if [ ! -d ".codeql/dbs/javascript-typescript" ]; then + echo "Error: JS/TS database not found. Create it first with create_js_ts_db" + return 1 + fi + init_codeql_dirs + codeql database analyze .codeql/dbs/javascript-typescript \ + codeql/javascript-queries:codeql-suites/javascript-security-extended.qls \ + --download \ + --format=sarif-latest \ + --output=.codeql/results/codeql-javascript-typescript-security.sarif + echo "โœ“ JavaScript/TypeScript security analysis complete" +} + +# ============================================================================ +# REANALYSIS (WITH DATABASE RECREATION) +# ============================================================================ + +# Reanalyze Python (recreate database) +reanalyze_python() { + echo "Reanalyzing Python (with database recreation)..." + rm -rf .codeql/dbs/python + create_python_db + analyze_python_default + echo "โœ“ Python reanalysis complete: .codeql/results/codeql-python-after.sarif" + # Rename for comparison + if [ -f ".codeql/results/codeql-python.sarif" ]; then + mv .codeql/results/codeql-python.sarif .codeql/results/codeql-python-after.sarif + fi +} + +# Reanalyze JavaScript/TypeScript (recreate database) +reanalyze_js_ts() { + echo "Reanalyzing JavaScript/TypeScript (with database recreation)..." + rm -rf .codeql/dbs/javascript-typescript + create_js_ts_db + analyze_js_ts_default + echo "โœ“ JavaScript/TypeScript reanalysis complete" + # Rename for comparison + if [ -f ".codeql/results/codeql-javascript-typescript.sarif" ]; then + mv .codeql/results/codeql-javascript-typescript.sarif .codeql/results/codeql-javascript-typescript-after.sarif + fi +} + +# ============================================================================ +# FULL WORKFLOW +# ============================================================================ + +# Run complete CodeQL workflow for a language +full_workflow_python() { + echo "=== Starting Python CodeQL workflow ===" + check_codeql || return 1 + create_python_db + analyze_python_default + echo "=== Python workflow complete ===" +} + +full_workflow_js_ts() { + echo "=== Starting JavaScript/TypeScript CodeQL workflow ===" + check_codeql || return 1 + create_js_ts_db + analyze_js_ts_default + echo "=== JavaScript/TypeScript workflow complete ===" +} + +# ============================================================================ +# MAIN +# ============================================================================ + +# If this script is sourced, make functions available +# If executed, run the specified function +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo "" + echo "Commands:" + echo " check_codeql Check CodeQL availability" + echo " init_codeql_dirs Create .codeql directories" + echo " create_python_db Create Python database" + echo " create_js_ts_db Create JavaScript/TypeScript database" + echo " validate_db Validate a database" + echo " analyze_python_default Analyze Python (security + quality)" + echo " analyze_python_security Analyze Python (security only)" + echo " analyze_js_ts_default Analyze JS/TS (security + quality)" + echo " analyze_js_ts_security Analyze JS/TS (security only)" + echo " reanalyze_python Reanalyze Python (recreate DB)" + echo " reanalyze_js_ts Reanalyze JS/TS (recreate DB)" + echo " full_workflow_python Complete Python workflow" + echo " full_workflow_js_ts Complete JS/TS workflow" + exit 1 + fi + + "$@" +fi diff --git a/.copilot/skills/codeql-analysis-remediation/assets/report-template.md b/.copilot/skills/codeql-analysis-remediation/assets/report-template.md new file mode 100644 index 00000000..39e0375f --- /dev/null +++ b/.copilot/skills/codeql-analysis-remediation/assets/report-template.md @@ -0,0 +1,220 @@ +# CodeQL remediation report template + +Use this template to summarize the CodeQL analysis and remediation effort. + +--- + +# CodeQL Remediation Report + +**Date:** [YYYY-MM-DD] +**Repository:** [Repository name] +**Branch:** [Branch name] +**Analyzed by:** [Agent name or person] + +## Executive Summary + +[1-2 sentences summarizing the effort, findings, and outcome] + +Example: +- "Analyzed repository for security and code quality issues with CodeQL. Found 12 Python findings, fixed 8 high-confidence issues, deferred 4 for future work. Repository verification passed." + +## Scope + +| Language | Analyzed | Finding count | Database | +|----------|----------|---------------|----------| +| Python | โœ“ | N | [Reused / Created] | +| JavaScript/TypeScript | โœ— | - | - | + +**Reason for language selection:** +- Python: `*.py` files and `pyproject.toml` present +- JavaScript/TypeScript: Not present in repository + +**CodeQL CLI:** [version number, e.g., "2.14.6"] + +## Findings summary + +### Python (if analyzed) + +| Severity | Total | Fixed | False positive | Deferred | Notes | +|----------|-------|-------|-----------------|----------|-------| +| Error | X | X | X | X | High-confidence security issues | +| Warning | X | X | X | X | Moderate or lower-priority findings | +| Note | X | X | X | X | Informational findings | +| **Total** | **X** | **X** | **X** | **X** | | + +### JavaScript/TypeScript (if analyzed) + +| Severity | Total | Fixed | False positive | Deferred | Notes | +|----------|-------|-------|-----------------|----------|-------| +| Error | X | X | X | X | | +| Warning | X | X | X | X | | +| Note | X | X | X | X | | +| **Total** | **X** | **X** | **X** | **X** | | + +## Findings addressed + +### Fixed findings + +List each fixed finding briefly: + +| Rule ID | File | Line | Severity | Summary | Status | +|---------|------|------|----------|---------|--------| +| `py/sql-injection` | `app/db/queries.py` | 28 | Error | Unsanitized user input in SQL query | โœ“ Fixed | +| `py/path-traversal` | `app/files.py` | 15 | Error | Unvalidated file path access | โœ“ Fixed | + +### False positives + +If any findings were determined to be false positives, explain why: + +| Rule ID | File | Line | Reason | +|---------|------|------|--------| +| `py/regex-dos` | `utils/validate.py` | 42 | Pattern is bounded by input length and validated before use | + +### Deferred findings + +Findings that were not addressed: + +| Rule ID | File | Severity | Reason | +|---------|------|----------|--------| +| `py/hardcoded-secret` | `config/defaults.py` | Warning | Placeholder API key in example code; requires security review for decision | + +## Changes made + +### Summary + +[Narrative summary of the changes made, grouped by finding or module] + +Example: +``` +Fixed 2 SQL injection issues by: +- Replacing string concatenation with parameterized queries in app/db/queries.py +- Adding input validation at the request handler level for user IDs + +Fixed 1 path traversal issue by: +- Validating file paths against base directory in app/files.py +- Adding security test to prevent regression + +Determined 1 finding to be a false positive: +- py/regex-dos: regex pattern is bounded by input validation and length checks +``` + +### Files modified + +- `app/db/queries.py` โ€” SQL injection fixes +- `app/db/models.py` โ€” Type hints for user ID parameter +- `app/files.py` โ€” Path validation for file access +- `tests/test_db.py` โ€” Added security tests +- `tests/test_files.py` โ€” Added path traversal prevention tests + +### Code examples (optional) + +If significant fixes were made, show before/after examples: + +**Before:** +```python +def get_user(user_id): + query = f"SELECT * FROM users WHERE id = {user_id}" + return db.execute(query) +``` + +**After:** +```python +def get_user(user_id: int) -> dict: + query = "SELECT * FROM users WHERE id = ?" + return db.execute(query, (user_id,)) +``` + +## Verification + +### Local verification + +Run after changes: + +| Command | Result | Notes | +|---------|--------|-------| +| `uv run pytest` | โœ“ PASS | 47 tests passed (5 new) | +| `uv run mypy . --config-file=pyproject.toml` | โœ“ PASS | No type errors | +| `uv run ruff check .` | โœ“ PASS | All linting checks pass | +| `uv run pre-commit run --all-files` | โœ“ PASS | All hooks pass | + +### CodeQL reanalysis + +After changes, reran CodeQL to verify findings were fixed: + +- **Python reanalysis:** โœ“ PASS + - Before: 12 findings + - After: 4 findings (8 fixed) + - Targeted findings: All resolved + +### Repository functionality + +- [ ] All tests pass +- [ ] Type checking passes +- [ ] Linting passes +- [ ] Project builds/runs (if applicable) +- [ ] No unrelated changes introduced + +## Impact assessment + +### Security improvements + +- Fixed 8 high-confidence security issues +- Added 5 security-focused test cases +- Reduced exposure to injection attacks, path traversal, and data exposure risks + +### Code quality improvements + +- Improved type annotations (all user IDs now explicitly typed as `int`) +- Better input validation at trust boundaries +- More comprehensive test coverage + +### Maintenance and future work + +- Deferred findings require security review before remediation +- Future developers should follow established secure patterns shown in the fixes +- Consider adding developer documentation on secure coding practices used in the fixes + +## Recommendations + +### Immediate actions (required to complete this effort) + +- โœ“ Merge changes and close related issues +- โœ“ Run full CI/CD pipeline in main branch +- โœ“ Deploy changes to production (if applicable) + +### Follow-up actions (future sprints) + +- Review and remediate deferred findings (e.g., hardcoded secrets) +- Update developer documentation with secure coding patterns used in fixes +- Consider running CodeQL analysis as part of regular CI/CD (not just ad-hoc) + +### Preventive measures + +- Enable CodeQL scanning in GitHub Actions (if not already enabled) +- Add security training focused on injection attacks and input validation +- Establish code review checklist for security considerations + +## Metadata + +| Property | Value | +|----------|-------| +| **Remediation effort** | ~4 hours (analysis + fixes + verification) | +| **Number of coder agents** | 2 (delegated fixes) | +| **Parallel tasks** | 4 (SQL injection, path traversal, false positive analysis, deferred review) | +| **Files committed** | 6 (4 source, 2 test) | +| **Lines added** | ~50 | +| **Lines removed** | ~20 | + +## Sign-off + +- **Analysis complete:** โœ“ [Date] +- **All fixes verified:** โœ“ [Date] +- **Tests passing:** โœ“ [Date] +- **Ready for merge:** โœ“ [Date] + +--- + +**Related documents:** +- [CodeQL workflow reference](../references/codeql-workflow.md) +- [SARIF interpretation guide](../references/sarif-interpretation.md) +- [Safe remediation rules](../references/safe-remediation-rules.md) diff --git a/.copilot/skills/codeql-analysis-remediation/assets/task-delegate-template.md b/.copilot/skills/codeql-analysis-remediation/assets/task-delegate-template.md new file mode 100644 index 00000000..56b7d93a --- /dev/null +++ b/.copilot/skills/codeql-analysis-remediation/assets/task-delegate-template.md @@ -0,0 +1,145 @@ +# Task delegation template for CodeQL remediation + +Use this template when delegating a fix to a coder agent. + +--- + +## Task for coder agent: [SHORT TITLE] + +### Objective + +[What must be fixed? One or two sentences.] + +Example: +- "Replace unsanitized user input in SQL query with parameterized query" +- "Add input validation at endpoint handler before passing to database layer" + +### CodeQL context + +Provide the specific finding that triggered this task: + +- **Rule ID:** `py/sql-injection` (or similar) +- **Severity:** Error / Warning / Note +- **File:** `src/database/models.py` +- **Line:** 42 +- **Message:** "[Short message from CodeQL]" +- **Code flow:** [Brief description of how data flows unsafely, if available] + +Example: +``` +- Rule ID: py/sql-injection +- Severity: Error +- File: app/db/queries.py +- Line: 28 +- Message: SQL query built using unsanitized user input from request parameter +``` + +### Scope + +**Allowed files to modify:** +- `app/db/queries.py` +- `app/db/models.py` +- `tests/test_db.py` (add/update tests) + +**Do not modify:** +- Unrelated files +- Generated files +- Lock files (unless dependency changes are explicitly required) +- Public API signatures (unless the task explicitly requires a change) + +### Implementation constraints + +- โœ… Prefer parameterized queries, safe path handling, or other established secure patterns +- โœ… Preserve public API behavior (function signatures, return types) unless the finding proves it unsafe +- โœ… Keep changes minimal โ€” fix the issue, do not refactor unrelated code +- โœ… Add or update tests where practical +- โœ… Use type hints and docstrings +- โŒ Do not suppress CodeQL findings (e.g., `# noqa`) unless there is documented strong justification +- โŒ Do not add broad exception handlers that hide errors +- โŒ Do not disable security checks to make tests pass + +### Verification + +Run the most relevant commands for this codebase: + +**Python repositories:** +```bash +uv run pytest # Run tests +uv run mypy . --config-file=pyproject.toml # Type check +uv run ruff check . # Lint +``` + +**JavaScript/TypeScript repositories:** +```bash +npm test # Run tests +npm run typecheck # Type check +npm run lint # Lint +``` + +**Both:** +```bash +git diff HEAD # Review changes +``` + +Ensure: +- All tests pass +- Type checking passes +- Linting passes +- No unrelated changes are included + +### Output required + +Provide: + +1. **Summary of changes** โ€” What was fixed and how? +2. **Files modified** โ€” Which files changed? +3. **Tests added/updated** โ€” Did you test the fix? +4. **Verification results** โ€” Which commands passed? +5. **Remaining risks or notes** โ€” Any concerns about the fix? + +Example output: +```markdown +## Changes made + +- Fixed SQL injection in `app/db/queries.py:28` by replacing string concatenation with parameterized query +- Updated `get_user_by_id()` to accept user_id as integer (already enforced by type hints) +- Added test case `test_user_query_with_special_characters()` to verify safety + +## Files modified + +- `app/db/queries.py` +- `tests/test_db.py` + +## Verification + +- `pytest`: โœ“ All tests pass (5 new, 42 existing) +- `mypy`: โœ“ No type errors +- `ruff`: โœ“ All checks pass +- `git diff`: Shows only intended changes (no unrelated refactoring) + +## Notes + +- The fix is minimal: only changed the query execution, no API changes +- Input validation already present at the request handler level +- No security checks disabled +``` + +--- + +## Tips for the orchestration agent + +When creating a task: + +1. **Be specific about scope** โ€” Clearly list which files can and cannot be modified +2. **Explain the context** โ€” Include the CodeQL finding, not just a generic description +3. **Define success criteria** โ€” What should verification look like? +4. **Keep tasks independent** โ€” Avoid assigning multiple agents to edit the same file +5. **Provide examples** โ€” If safe patterns are unclear, show what good looks like + +When reviewing completed tasks: + +1. **Check the diff** โ€” Does it fix the issue without unrelated changes? +2. **Verify tests** โ€” Are there new tests? Do they pass? +3. **Confirm constraints** โ€” Did the agent respect the scope? +4. **Check safety** โ€” Does the fix use established secure patterns? +5. **Review the output** โ€” Is the explanation clear and accurate? diff --git a/.copilot/skills/codeql-analysis-remediation/references/codeql-workflow.md b/.copilot/skills/codeql-analysis-remediation/references/codeql-workflow.md new file mode 100644 index 00000000..1c549915 --- /dev/null +++ b/.copilot/skills/codeql-analysis-remediation/references/codeql-workflow.md @@ -0,0 +1,230 @@ +# CodeQL Workflow: Step-by-step execution + +This document details the complete workflow for running CodeQL analysis and orchestrating remediation. + +## Step 1: Repository inspection + +Inspect the repository structure to determine which languages are applicable. + +**Python indicators:** +- `*.py` files +- `pyproject.toml`, `requirements.txt`, `setup.py`, `setup.cfg`, `Pipfile`, `uv.lock`, `poetry.lock` + +**JavaScript/TypeScript indicators:** +- `*.js`, `*.jsx`, `*.ts`, `*.tsx` files +- `package.json`, `tsconfig.json`, `vite.config.*`, `next.config.*`, `webpack.config.*` + +Only run CodeQL for languages that are actually present. + +## Step 2: CodeQL CLI availability check + +Before running CodeQL, verify the CLI is available: + +```bash +codeql version +``` + +If CodeQL is not available, stop and report that CodeQL CLI must be installed first. Do not continue with simulated analysis. + +## Step 3: Database reuse policy + +Prefer reusing existing CodeQL databases where possible. + +Expected local database layout: + +``` +.codeql/ + dbs/ + python/ + javascript-typescript/ + results/ +``` + +Before creating a new database, check whether it already exists: + +```bash +test -d .codeql/dbs/python && echo "Python DB exists" +test -d .codeql/dbs/javascript-typescript && echo "JS/TS DB exists" +``` + +**A database can be reused when:** +- The directory exists +- CodeQL recognizes it +- There is no clear reason to believe it is stale (e.g., source files or dependencies changed significantly) + +**Validate a database:** + +```bash +codeql database print-baseline .codeql/dbs/python +# or attempt analysis +``` + +If the database is missing, corrupt, or not recognized, recreate only the affected language database. + +## Step 4: Database creation + +Create databases only for applicable languages. Do not pass a build command for Python or JavaScript/TypeScript unless the repository clearly requires one. + +### Create Python database: + +```bash +mkdir -p .codeql/dbs .codeql/results + +if [ ! -d ".codeql/dbs/python" ]; then + codeql database create .codeql/dbs/python \ + --language=python \ + --source-root=. +fi +``` + +### Create JavaScript/TypeScript database: + +```bash +mkdir -p .codeql/dbs .codeql/results + +if [ ! -d ".codeql/dbs/javascript-typescript" ]; then + codeql database create .codeql/dbs/javascript-typescript \ + --language=javascript-typescript \ + --source-root=. +fi +``` + +## Step 5: CodeQL analysis + +Run CodeQL analysis for each applicable language. + +### Analyze Python: + +```bash +codeql database analyze .codeql/dbs/python \ + codeql/python-queries:codeql-suites/python-security-and-quality.qls \ + --download \ + --format=sarif-latest \ + --output=.codeql/results/codeql-python.sarif +``` + +### Analyze JavaScript/TypeScript: + +```bash +codeql database analyze .codeql/dbs/javascript-typescript \ + codeql/javascript-queries:codeql-suites/javascript-security-and-quality.qls \ + --download \ + --format=sarif-latest \ + --output=.codeql/results/codeql-javascript-typescript.sarif +``` + +If `security-and-quality` produces excessive noise, rerun with the security-focused suite: + +```bash +codeql database analyze .codeql/dbs/python \ + codeql/python-queries:codeql-suites/python-security-extended.qls \ + --download \ + --format=sarif-latest \ + --output=.codeql/results/codeql-python.sarif +``` + +```bash +codeql database analyze .codeql/dbs/javascript-typescript \ + codeql/javascript-queries:codeql-suites/javascript-security-extended.qls \ + --download \ + --format=sarif-latest \ + --output=.codeql/results/codeql-javascript-typescript.sarif +``` + +## Step 6: SARIF interpretation + +Parse the SARIF output files. Extract: + +- Rule ID +- Severity or level +- Message +- File path and line number +- Code flow (if available) +- Whether the issue is actionable +- Whether it may be a false positive + +See [sarif-interpretation.md](sarif-interpretation.md) for detailed prioritization and classification logic. + +## Step 7: Create remediation plan + +Before assigning work, create a concise remediation plan: + +- Applicable languages found +- CodeQL databases reused or created +- Number of findings per language +- Findings selected for remediation +- Files likely affected +- Suggested task split for parallel coder agents +- Verification commands to run after changes + +Prefer small, isolated tasks. Avoid assigning multiple agents to edit the same file. + +## Step 8: Delegate to parallel coder agents + +Assign tasks to coder agents only after the plan is clear. Use the template in [../assets/task-delegate-template.md](../assets/task-delegate-template.md). + +Do not immediately edit code yourself unless the change is very small and does not benefit from delegation. + +## Step 9: Review changes + +After coder agents return their work: + +1. Inspect all diffs +2. Check whether each task actually addresses the finding +3. Check for regressions or unrelated changes +4. Check whether tests were added or updated where useful +5. Merge or adapt the changes as needed +6. Resolve conflicts between parallel changes + +If a coder agent introduces risky or unrelated changes, revert or narrow them. + +## Step 10: Reanalysis and verification + +After code changes, rerun CodeQL for affected languages. + +If source changes are substantial, recreate the affected database first: + +### Reanalyze Python (with database recreation): + +```bash +rm -rf .codeql/dbs/python + +codeql database create .codeql/dbs/python \ + --language=python \ + --source-root=. + +codeql database analyze .codeql/dbs/python \ + codeql/python-queries:codeql-suites/python-security-and-quality.qls \ + --download \ + --format=sarif-latest \ + --output=.codeql/results/codeql-python-after.sarif +``` + +### Reanalyze JavaScript/TypeScript (with database recreation): + +```bash +rm -rf .codeql/dbs/javascript-typescript + +codeql database create .codeql/dbs/javascript-typescript \ + --language=javascript-typescript \ + --source-root=. + +codeql database analyze .codeql/dbs/javascript-typescript \ + codeql/javascript-queries:codeql-suites/javascript-security-and-quality.qls \ + --download \ + --format=sarif-latest \ + --output=.codeql/results/codeql-javascript-typescript-after.sarif +``` + +**Compare before and after results** to confirm that targeted findings are resolved. + +Use language-specific verification commands from [verification-commands.md](verification-commands.md) to ensure the repository still works. + +## Step 11: Final report + +Generate a concise report using [../assets/report-template.md](../assets/report-template.md). + +A fix is complete only when: +- The targeted finding is gone, **or** +- The finding is proven to be a false positive and documented, **or** +- The remaining risk requires explicit human product or security decision diff --git a/.copilot/skills/codeql-analysis-remediation/references/safe-remediation-rules.md b/.copilot/skills/codeql-analysis-remediation/references/safe-remediation-rules.md new file mode 100644 index 00000000..c6df43ac --- /dev/null +++ b/.copilot/skills/codeql-analysis-remediation/references/safe-remediation-rules.md @@ -0,0 +1,305 @@ +# Safe Remediation Rules + +This document defines the safe practices for fixing CodeQL findings. Follow these rules to ensure that fixes are effective, maintainable, and do not introduce regressions. + +## General principles + +### Principle 1: Fix at the trust boundary + +Prefer input validation at trust boundaries (where external input enters the system) over scattered internal checks. + +**Good:** +```python +@app.route('/api/user/') +def get_user(user_id: str): + # Validate at boundary + user_id_int = _parse_user_id(user_id) + return fetch_user_from_db(user_id_int) + +def _parse_user_id(value: str) -> int: + """Parse and validate user ID at trust boundary.""" + parsed = int(value) + if parsed < 0 or parsed > 1000000: + raise ValueError("Invalid user ID") + return parsed +``` + +**Avoid:** +```python +def fetch_user_from_db(user_id): + # Scattered validation in implementation + if isinstance(user_id, str): + user_id = int(user_id) + if user_id < 0: + return None + query = f"SELECT * FROM users WHERE id = {user_id}" + return db.execute(query) +``` + +### Principle 2: Use safe patterns + +When multiple approaches exist, prefer established safe patterns: + +- **SQL:** Use parameterized queries, never string concatenation +- **Path operations:** Use `pathlib.Path`, validate against allowlist +- **XSS:** Escape at output boundary, use templating libraries +- **Serialization:** Use JSON, avoid pickle/YAML unless absolutely necessary +- **Regex:** Use bounded patterns, avoid complex backtracking + +### Principle 3: Minimize scope of changes + +Make the narrowest possible change that fixes the issue. Do not refactor unrelated code. + +**Good:** +```python +# Before +query = f"SELECT * FROM users WHERE id = {user_id}" + +# After (narrow fix) +query = "SELECT * FROM users WHERE id = ?" +cursor.execute(query, (user_id,)) +``` + +**Avoid:** +```python +# Before +query = f"SELECT * FROM users WHERE id = {user_id}" + +# After (broad, unnecessary refactor) +def build_query(table, condition_column, condition_value): + # Completely rewrote the function + # Changed behavior, added new parameters + # Much harder to review and test + ... +``` + +### Principle 4: Preserve public API behavior + +Unless the finding proves that the API behavior is unsafe, preserve existing function signatures and return types. + +**Good:** +```python +# Before (unsafe but public API) +def get_user(user_id): + return db.query(f"SELECT * FROM users WHERE id = {user_id}") + +# After (safe, same API) +def get_user(user_id: int) -> dict: + return db.query("SELECT * FROM users WHERE id = ?", (user_id,)) +``` + +**Avoid:** +```python +# Before +def get_user(user_id): + return db.query(f"SELECT * FROM users WHERE id = {user_id}") + +# After (changed API unnecessarily) +def get_user_safe(user_id: int, db_conn: Connection) -> Optional[dict]: + # Changed function name, added parameter, changed return type + # This breaks all callers + ... +``` + +## Category-specific rules + +### SQL Injection fixes + +**Rule:** Always use parameterized queries. Never concatenate user input into SQL strings. + +**Bad:** +```python +query = f"SELECT * FROM users WHERE id = {user_id}" +result = db.execute(query) +``` + +**Good:** +```python +query = "SELECT * FROM users WHERE id = ?" +result = db.execute(query, (user_id,)) +``` + +### Command injection fixes + +**Rule:** Avoid shell=True and string concatenation. Use explicit argument lists. + +**Bad:** +```python +import subprocess +subprocess.run(f"ls {directory}", shell=True) +``` + +**Good:** +```python +import subprocess +subprocess.run(["ls", directory]) +``` + +### Path traversal fixes + +**Rule:** Validate file paths against an allowlist or restrict to a base directory. + +**Bad:** +```python +def read_file(filename): + with open(filename, 'r') as f: + return f.read() + +read_file('../../../etc/passwd') # Escapes the intended directory +``` + +**Good:** +```python +from pathlib import Path + +BASE_DIR = Path('/app/data') + +def read_file(filename: str) -> str: + requested = (BASE_DIR / filename).resolve() + if not str(requested).startswith(str(BASE_DIR)): + raise ValueError(f"Path {filename} escapes base directory") + return requested.read_text() + +# read_file('../../../etc/passwd') now raises ValueError +``` + +### XSS fixes + +**Rule:** Escape output at the boundary (near the template or response). Use templating libraries that escape by default. + +**Bad:** +```python +# Flask +@app.route('/user/') +def greet(name): + return f"

Hello {name}

" +``` + +**Good:** +```python +# Flask with Jinja2 (escapes by default) +@app.route('/user/') +def greet(name): + return render_template('greet.html', name=name) + +# greet.html +

Hello {{ name }}

+``` + +### Deserialization fixes + +**Rule:** Avoid pickle and unsafe YAML loading. Use JSON or validated schemas. + +**Bad:** +```python +import pickle +data = pickle.loads(user_input) # Arbitrary code execution risk +``` + +**Good:** +```python +import json +from pydantic import BaseModel + +class UserData(BaseModel): + name: str + age: int + +data = UserData(**json.loads(user_input)) +``` + +## Code quality rules + +### Do not suppress findings + +**Bad:** +```python +# noqa: py/sql-injection +query = f"SELECT * FROM users WHERE id = {user_id}" +db.execute(query) +``` + +Suppressing a finding without fixing it defers the problem and trains developers to ignore warnings. + +If the finding is truly a false positive, instead: +1. Add a code change that clarifies why it is safe +2. Add a test that validates the safety assumption +3. Document the reasoning + +**Better:** +```python +# Ensure user_id is always an integer (type-safe in Python 3.9+) +def get_user(user_id: int) -> dict: + # CodeQL understands type narrowing; no suppression needed + query = f"SELECT * FROM users WHERE id = {user_id}" + return db.execute(query) +``` + +### Do not add blanket exception handlers + +**Bad:** +```python +try: + query = f"SELECT * FROM users WHERE id = {user_id}" + return db.execute(query) +except Exception: + return None # Hides all errors, including security errors +``` + +Broad `except` clauses hide real problems and make debugging impossible. + +**Good:** +```python +try: + user_id_int = int(user_id) +except ValueError: + raise ValueError(f"Invalid user ID: {user_id}") + +query = "SELECT * FROM users WHERE id = ?" +return db.execute(query, (user_id_int,)) +``` + +### Do not disable security checks to make tests pass + +**Bad:** +```python +# In test, disable CSRF protection just to make the test pass +@csrf.exempt +def test_post_endpoint(): + response = client.post('/api/data', data=...) +``` + +If a test requires disabling security, the issue is with the test, not the security check. + +**Good:** +```python +# Generate valid CSRF token for the test +def test_post_endpoint(): + response = client.get('/form') + csrf_token = extract_csrf_token(response) + response = client.post('/api/data', data=..., headers={'X-CSRFToken': csrf_token}) +``` + +## Delegation rules for coder agents + +When delegating a fix to a coder agent, include these constraints: + +- **Preserve public API behavior** unless the task explicitly requires a change +- **Keep changes minimal** โ€” fix the issue, do not refactor unrelated code +- **Add or update tests** where practical +- **Do not suppress findings** unless there is a strong reason documented in the code +- **If the finding is a false positive,** document why instead of making a risky change + +## Review checklist for code changes + +Before accepting changes from coder agents: + +1. โœ… **Does the fix address the CodeQL finding?** โ€” The change should make the issue go away. +2. โœ… **Is the fix safe?** โ€” Does it use established secure patterns? Did it introduce new risks? +3. โœ… **Is the change minimal?** โ€” Only what was necessary, no unrelated refactoring? +4. โœ… **Are tests added or updated?** โ€” Does new test coverage validate the fix? +5. โœ… **Is the public API preserved?** โ€” Did function signatures and behavior change unnecessarily? +6. โœ… **Is documentation updated?** โ€” Do comments explain the why, not just the what? +7. โœ… **Are regressions unlikely?** โ€” Could this change break existing code? + +If any of these questions raise concerns, ask the coder agent to refine the changes. diff --git a/.copilot/skills/codeql-analysis-remediation/references/sarif-interpretation.md b/.copilot/skills/codeql-analysis-remediation/references/sarif-interpretation.md new file mode 100644 index 00000000..9ecff0ee --- /dev/null +++ b/.copilot/skills/codeql-analysis-remediation/references/sarif-interpretation.md @@ -0,0 +1,154 @@ +# SARIF Interpretation and Finding Classification + +This document explains how to interpret CodeQL SARIF output and classify findings for remediation. + +## Extracting findings from SARIF + +After CodeQL analysis completes, parse the SARIF output to extract: + +- **Rule ID** โ€” CodeQL rule identifier (e.g., `py/sql-injection`) +- **Severity or level** โ€” Error, Warning, Note +- **Message** โ€” Human-readable description of the issue +- **File path** โ€” Which file contains the issue +- **Line number** โ€” Exact location in the file +- **Code flow** โ€” If available, the data flow path leading to the issue +- **Actionability** โ€” Is this a real, actionable issue? +- **False positive likelihood** โ€” Might this be a false positive? + +## Finding prioritization + +Prioritize findings in this order. Start with high-priority items; defer lower-priority items unless explicitly directed. + +### Tier 1: Critical Security Issues (Fix now) + +1. **High confidence security issues** โ€” Clear, unmistakable security flaws +2. **Injection risks** โ€” SQL injection, command injection, expression language injection +3. **Authentication and authorization issues** โ€” Broken access control, missing auth checks +4. **Secrets or credential handling issues** โ€” Hardcoded passwords, API keys, tokens + +### Tier 2: Important Security Issues (Fix after tier 1) + +5. **Deserialization issues** โ€” Unsafe pickle, unsafe YAML loading, unsafe XML parsing +6. **Path traversal or file handling issues** โ€” Directory traversal, unsafe file operations +7. **Cross site scripting (XSS)** โ€” Unescaped output, DOM-based XSS +8. **Server side request forgery (SSRF)** โ€” Unvalidated external requests + +### Tier 3: Moderate Issues (Review and decide) + +9. **Regular expression denial of service (ReDoS)** โ€” Uncontrolled regex patterns +10. **Information disclosure** โ€” Secrets in logs, sensitive data exposure +11. **Lower severity quality findings** โ€” Code quality, maintainability, best practices + +## Finding classification + +For each finding, classify it into one of four categories: + +### Fix now +- High-confidence, actionable security issues +- Clear breach of safe coding practice +- Minimal risk of introducing regressions +- Strong understanding of the fix + +**Example:** Hardcoded database password detected in source code. + +### Needs human decision +- Findings that depend on business logic or threat model +- Issues that require architectural decisions +- Findings where the "correct" behavior is ambiguous +- Issues requiring security team review + +**Example:** Whether an unauthenticated endpoint is intentional or a security flaw. + +### False positive +- CodeQL flagged benign code +- The issue is proven safe by surrounding context +- The reported risk does not apply to this codebase + +**Example:** CodeQL flags a regex as ReDoS, but the pattern is bounded by the input source. + +Document *why* it is a false positive so future reviewers understand the reasoning. + +### Defer +- Lower-priority quality findings +- Issues that should be addressed but are not critical +- Technical debt that can be scheduled separately +- Findings requiring significant refactoring + +## Safe assumptions about findings + +When interpreting a finding: + +1. **Trust CodeQL's data flow analysis** โ€” If CodeQL reports a data flow, it typically has traced it carefully. Do not dismiss it without understanding the flow. + +2. **Be skeptical of lenient context** โ€” CodeQL may not understand: + - Custom validation functions (e.g., your own `sanitize()` function) + - External security boundaries (e.g., firewalls, WAFs) + - Complex conditional logic that limits actual risk + +3. **Prioritize trust boundaries** โ€” Issues at entry points (user input, network, files) are more serious than issues in internal logic. + +4. **Distinguish between risk and practicality** โ€” A finding may be theoretically exploitable but practically impossible to trigger. Document this distinction. + +## Recording decisions + +When classifying a finding, record: + +- **Finding ID and rule** โ€” What CodeQL rule was violated? +- **Classification** โ€” Fix now, needs decision, false positive, or defer +- **Reasoning** โ€” Why did you make this classification? +- **Owner** โ€” Who is responsible for the outcome? +- **Timeline** โ€” When should this be addressed? + +Example decision record: + +```markdown +### py/sql-injection in db/models.py:42 + +- **Rule:** `py/sql-injection` +- **Classification:** Fix now +- **Reasoning:** Unsanitized user input concatenated into SQL query. High likelihood of real exploitation. +- **Owner:** Backend team +- **Timeline:** This sprint +``` + +## Handling false positives + +If a finding is a false positive: + +1. Document *why* it is a false positive +2. Understand what CodeQL misunderstood +3. Do not suppress the finding with a code comment like `# noqa: py/sql-injection` +4. Instead: + - Verify the finding is indeed safe + - Consider whether clarifying the code would prevent future false positives + - Make a small code change to add clarity (e.g., extracting a well-named helper function) + - Add a test or assertion that validates the safety assumption + +Example approach: + +```python +# Before: CodeQL flags this as unsafe +query = "SELECT * FROM users WHERE id = " + str(user_id) + +# After: CodeQL understands this is safe due to explicit type conversion and bounds +user_id_int = int(user_id) # Raises ValueError if not numeric +if user_id_int < 0 or user_id_int > 1000000: + raise ValueError("Invalid user ID") +query = f"SELECT * FROM users WHERE id = {user_id_int}" +``` + +## Batch interpreting multiple findings + +When processing many findings: + +1. **Group by rule** โ€” Organize findings by rule ID to see patterns +2. **Group by file** โ€” Understand which areas have the most issues +3. **Group by severity** โ€” Address high-severity first +4. **Identify common patterns** โ€” Look for systemic issues (e.g., all injection flaws are in one module) + +Common patterns often suggest: +- A shared responsibility (e.g., one team's code) +- A systemic process gap (e.g., input validation layer is missing) +- A training opportunity (e.g., developers unaware of the risk) + +Use these patterns to inform the remediation plan. diff --git a/.copilot/skills/codeql-analysis-remediation/references/verification-commands.md b/.copilot/skills/codeql-analysis-remediation/references/verification-commands.md new file mode 100644 index 00000000..b6653137 --- /dev/null +++ b/.copilot/skills/codeql-analysis-remediation/references/verification-commands.md @@ -0,0 +1,182 @@ +# Language-specific verification commands + +After making code changes to fix CodeQL findings, run relevant verification commands to ensure the repository still works. + +## Python verification + +Before running commands, check what tools and scripts the project supports. + +**Inspect for:** +- `pyproject.toml` โ€” Python project metadata +- `pytest.ini` โ€” pytest configuration +- `tox.ini` โ€” tox automation config +- `noxfile.py` โ€” nox automation config +- `Makefile` โ€” Make targets +- `uv.lock` โ€” uv lock file (indicates uv usage) + +### Possible Python verification commands + +| Command | Purpose | +|---------|---------| +| `uv run pytest` | Run tests via uv | +| `pytest` | Run tests directly | +| `python -m pytest` | Run tests via Python module | +| `uv run pytest --cov` | Run tests with coverage via uv | +| `pytest --cov` | Run tests with coverage | +| `uv run ruff check .` | Lint with ruff via uv | +| `ruff check .` | Lint with ruff directly | +| `uv run black --check .` | Check code formatting via uv | +| `black --check .` | Check code formatting directly | +| `uv run mypy .` | Type check via uv | +| `mypy .` | Type check directly | +| `uv run pre-commit run --all-files` | Run all pre-commit hooks via uv | +| `pre-commit run --all-files` | Run all pre-commit hooks directly | + +**Recommended order:** +1. `uv run pytest` (or `pytest`) โ€” Unit tests first +2. `uv run mypy .` (or `mypy .`) โ€” Type checking +3. `uv run ruff check . --fix` โ€” Linting and auto-fixes +4. `uv run black .` (or `black .`) โ€” Code formatting + +If any command fails: +- Determine whether the failure is caused by your changes or was pre-existing +- Do not claim the repository works unless verification passed +- Report the failure and any relevant logs + +### Example Python verification session + +```bash +# Run tests +uv run pytest +# โœ“ Tests pass + +# Type check +uv run mypy . --config-file=pyproject.toml +# โœ“ No type errors + +# Lint +uv run ruff check . --fix +# โœ“ All checks pass + +# Pre-commit hooks (if using them) +uv run pre-commit run --all-files +# โœ“ All hooks pass + +echo "โœ“ Python verification complete" +``` + +## JavaScript / TypeScript verification + +Before running commands, inspect `package.json` to see available scripts. + +**Inspect for:** +- `package.json` โ€” Project metadata and scripts +- `tsconfig.json` โ€” TypeScript configuration +- `vite.config.*` โ€” Vite configuration +- `next.config.*` โ€” Next.js configuration +- `webpack.config.*` โ€” Webpack configuration +- `jest.config.*` โ€” Jest configuration + +### Possible JavaScript/TypeScript verification commands + +| Command | Purpose | +|---------|---------| +| `npm test` | Run tests | +| `npm run test` | Run tests (alternate) | +| `npm run lint` | Lint code | +| `npm run lint:fix` | Lint and auto-fix | +| `npm run typecheck` | Type check TypeScript | +| `npm run build` | Build project | +| `npm run format` | Format code | +| `npm run format:check` | Check code formatting | + +**Recommended order:** +1. `npm run lint` (or `npm run lint:fix`) โ€” Linting +2. `npm run typecheck` โ€” TypeScript type checking +3. `npm test` โ€” Unit tests +4. `npm run build` โ€” Build (if applicable) + +If any command fails: +- Determine whether the failure is caused by your changes or was pre-existing +- Do not claim the repository works unless verification passed +- Report the failure and any relevant logs + +### Example JavaScript/TypeScript verification session + +```bash +# Lint and auto-fix +npm run lint:fix +# โœ“ Linting passed + +# Type check +npm run typecheck +# โœ“ No type errors + +# Run tests +npm test +# โœ“ All tests pass + +# Build +npm run build +# โœ“ Build succeeded + +echo "โœ“ JavaScript/TypeScript verification complete" +``` + +## Repository state verification + +For any language, check basic repository health: + +1. **No uncommitted changes from pre-commit hooks:** + ```bash + git status + # Should show no "modified" files if formatting/linting hooks auto-fixed issues + # Should show no unexpected files + ``` + +2. **No syntax errors in primary files:** + - Python: `python -m py_compile ` + - JavaScript: `node -c ` + - TypeScript: `tsc --noEmit` + +3. **No circular imports or module issues:** + - Python: `python -c "import "` + - JavaScript: Try importing the main entry point + +## Handling verification failures + +If a verification command fails: + +1. **Understand the failure:** + - Read the error message carefully + - Check whether it is related to your changes + - Verify the failure is not pre-existing + +2. **Distinguish between categories:** + - **Your change broke it:** Revert or fix your change + - **Pre-existing failure:** Report it separately; do not claim it as part of your work + - **Environment issue:** Fix the environment (install missing dependencies, etc.) + +3. **Report failure clearly:** + - Command that failed + - Exit code + - Error output + - Whether it is pre-existing + - Whether it blocks your fix + +## When verification is optional + +Some checks are optional depending on the project: + +- Build commands (`npm run build`) may not exist for libraries +- TypeScript checking may not apply to JavaScript-only projects +- Coverage thresholds may not be enforced +- Some linters may be optional + +Only run commands that are actually configured in the project. + +If in doubt, run: +- **Python:** `uv run pytest` +- **JavaScript/TypeScript:** `npm test` + +These are the most common, lowest-risk verification points. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 50deecb7..152675c3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -54,7 +54,8 @@ }, "mounts": [ "source=python-template-uv-cache,target=/home/vscode/.cache/uv,type=volume", - "source=python-template-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume" + "source=python-template-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume", + "source=${localEnv:HOME},target=/host-home,type=bind,consistency=cached" ], "name": "python-basic-template", "postCreateCommand": "bash .devcontainer/post-create.sh", diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 6e517d41..58629efc 100644 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -4,6 +4,27 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${repo_root}" +# Expose the host ~/.copilot directory inside the devcontainer. +# This assumes devcontainer.json mounts the host home directory to /host-home. +if [[ -d /host-home ]]; then + mkdir -p /host-home/.copilot + + container_copilot_dir="${HOME}/.copilot" + host_copilot_dir="/host-home/.copilot" + + if [[ -L "${container_copilot_dir}" ]]; then + echo "Copilot directory symlink already exists at ${container_copilot_dir}." + elif [[ -e "${container_copilot_dir}" ]]; then + echo "Copilot directory already exists at ${container_copilot_dir}; not replacing it." + echo "To use the host Copilot directory, move this folder aside and link it to ${host_copilot_dir}." + else + ln -s "${host_copilot_dir}" "${container_copilot_dir}" + echo "Linked ${container_copilot_dir} to ${host_copilot_dir}." + fi +else + echo "Host home mount /host-home not found; skipping Copilot directory link." +fi + echo "Bootstrapping the container workspace with uv..." uv --version python --version diff --git a/pyproject.toml b/pyproject.toml index 5eca8842..bebb6e8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -194,7 +194,8 @@ skips = ["B101", "B601"] # Used by both pre-commit hooks and CI workflows via scripts/hooks/run-pip-audit.sh # See docs/6_pre_commit_troubleshooting.md for explanation of each exclusion ignore = [ - "CVE-2026-3219" # pip 26.0.1 pip handles concatenated tar and ZIP files as ZIP files regardless of filename or whether a file is both a tar and ZIP file. This behavior could result in confusing installation behavior, such as installing "incorrect" files according to the filename of the archive. New behavior only proceeds with installation if the file identifies uniquely as a ZIP or tar archive, not as both. + "CVE-2026-3219", # pip 26.0.1 pip handles concatenated tar and ZIP files as ZIP files regardless of filename or whether a file is both a tar and ZIP file. This behavior could result in confusing installation behavior, such as installing "incorrect" files according to the filename of the archive. New behavior only proceeds with installation if the file identifies uniquely as a ZIP or tar archive, not as both. + "CVE-2026-6357" ] [tool.uv] From a2cd86f0d6ce54fa5effef35048d75453086ba0e Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 6 May 2026 11:02:07 +0000 Subject: [PATCH 136/199] feat: update bandit configuration to set severity level and adjust exclusions --- .pre-commit-config.yaml | 4 ++-- bandit.yml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 bandit.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index edb1228d..6da0a7ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -93,8 +93,8 @@ repos: hooks: - id: bandit name: bandit (security check) - args: [-ll, --skip, B101] - exclude: ^tests/ + args: [--severity-level, high, --skip, B101] + exclude: ^migrations/ types: [python] # Docstring validation diff --git a/bandit.yml b/bandit.yml new file mode 100644 index 00000000..81c6d092 --- /dev/null +++ b/bandit.yml @@ -0,0 +1,10 @@ +# Bandit config for ABS project +# Only report HIGH severity issues +# Skip B101 (unused imports) + +# severity +severity: HIGH + +# skip +skip: + - B101 From 19cf4de2a8c3e5e3caf921e95f3b66b1b6d0ac6d Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 6 May 2026 11:51:09 +0000 Subject: [PATCH 137/199] feat: refine CodeQL remediation workflow and verification commands for clarity and effectiveness --- .../codeql-analysis-remediation/SKILL.md | 51 ++++++++++++----- .../references/codeql-workflow.md | 50 +++++++++++++---- .../references/verification-commands.md | 55 +++++++++++++++---- 3 files changed, 121 insertions(+), 35 deletions(-) diff --git a/.copilot/skills/codeql-analysis-remediation/SKILL.md b/.copilot/skills/codeql-analysis-remediation/SKILL.md index d4e0968b..972663f6 100644 --- a/.copilot/skills/codeql-analysis-remediation/SKILL.md +++ b/.copilot/skills/codeql-analysis-remediation/SKILL.md @@ -2,24 +2,25 @@ ## Purpose -Use this skill when the task is to analyze the current repository with CodeQL, identify security or quality issues, plan safe fixes, delegate implementation work to parallel coder agents, and verify that the repository still works after changes. +Use this skill when the task is to analyze the current repository with CodeQL, identify security or quality issues, plan safe fixes, implement or delegate remediations as appropriate, and verify that the repository still works after changes. The goal is not only to run CodeQL, but to turn the findings into controlled, reviewable code improvements. ## Operating mode -You are the orchestration agent. Your default workflow is to: +You are the remediation lead. Your default workflow is to: 1. Inspect the repository and determine applicable languages 2. Check CodeQL CLI availability 3. Create or reuse CodeQL databases 4. Run CodeQL analysis -5. Interpret findings and create an implementation plan -6. Delegate independent tasks to parallel coder agents -7. Review changes and run verification checks -8. Produce a final remediation report +5. Interpret findings and choose the smallest safe remediation path +6. Fix locally when the change is narrow, or delegate independent tasks when parallel work is actually useful +7. Run the cheapest focused verification for each touched slice +8. Reanalyze and compare before/after results +9. Produce a final remediation report -**Do not immediately edit code.** Delegate all but the smallest changes to coder agents. +**Do not default to delegation.** Direct, local edits are preferred when a finding is confined to one file or one small slice of behavior. Delegate only when the findings split cleanly across files or subsystems, or when parallel work materially reduces turnaround time. ## Quick start @@ -30,6 +31,8 @@ Key steps: - **Supported languages:** Python (`python`), JavaScript/TypeScript (`javascript-typescript`) - **Database directory:** `.codeql/dbs/` (reuse if possible) - **Results directory:** `.codeql/results/` (SARIF format) +- **Compare results:** keep a baseline SARIF and write refreshed output to a distinct `*-after.sarif` file +- **Residual validation:** prefer narrow reruns for the exact remaining rule or language slice before paying for another full scan - **Finding prioritization:** See [references/sarif-interpretation.md](references/sarif-interpretation.md) - **Safe remediation rules:** See [references/safe-remediation-rules.md](references/safe-remediation-rules.md) @@ -40,6 +43,8 @@ Key steps: - Do not blindly fix every CodeQL finding; classify each finding first - Do not suppress findings without clear justification - Do not make broad refactors while fixing security findings +- Prefer the cheapest discriminating validation after each substantive fix +- Compare SARIF carefully; line shifts alone do not prove new findings - Always verify that the repository still works after changes ## Repository inspection checklist @@ -50,6 +55,20 @@ Key steps: **JavaScript/TypeScript indicators:** - `*.js`, `*.jsx`, `*.ts`, `*.tsx` files, `package.json`, `tsconfig.json`, `vite.config.*`, `next.config.*`, `webpack.config.*` +## Direct work vs delegation + +Default to direct remediation when: +- The finding is isolated to one file or one local control path +- A small reversible edit plus a narrow verification can confirm the fix +- The overhead of splitting context across agents would exceed the size of the change + +Delegate when: +- Findings split cleanly across multiple files or languages +- The changes are independent and can be reviewed in parallel +- One slice needs focused implementation while you continue scan comparison or review work + +When delegating, avoid assigning multiple agents to edit the same file. + ## Delegation to coder agents Use [assets/task-delegate-template.md](assets/task-delegate-template.md) as a template. @@ -61,16 +80,17 @@ Each task must include: - Verification commands - Expected output -Assign independent tasks to parallel coder agents. Avoid assigning multiple agents to edit the same file. +Assign independent tasks to parallel coder agents only when the split is clean. Avoid assigning multiple agents to edit the same file. ## Review and verification -After coder agents return changes: -1. Inspect all diffs -2. Check whether each task addresses the finding -3. Check for regressions or unrelated changes -4. Use language-specific verification commands (see [references/verification-commands.md](references/verification-commands.md)) +After each remediation, whether done locally or by a coder agent: +1. Inspect the diff for scope control +2. Check whether the change addresses the targeted finding +3. Run the cheapest focused verification first +4. Use language-specific verification commands when the touched slice justifies them (see [references/verification-commands.md](references/verification-commands.md)) 5. Rerun CodeQL for affected languages if changes are substantial +6. Compare before/after SARIF and call out residual findings, removed findings, and line-shift-only churn ## Final report @@ -81,6 +101,11 @@ A fix is complete only when: - The finding is proven to be a false positive and documented, **or** - The remaining risk requires explicit human product or security decision +When reporting results: +- Distinguish targeted fixes from unrelated findings that were left alone +- Note when a narrow query rerun was used to confirm a residual fix +- Call out any findings that appear "new" only because edited files shifted line numbers + --- **Related files:** diff --git a/.copilot/skills/codeql-analysis-remediation/references/codeql-workflow.md b/.copilot/skills/codeql-analysis-remediation/references/codeql-workflow.md index 1c549915..ad4b9aaf 100644 --- a/.copilot/skills/codeql-analysis-remediation/references/codeql-workflow.md +++ b/.copilot/skills/codeql-analysis-remediation/references/codeql-workflow.md @@ -1,6 +1,6 @@ # CodeQL Workflow: Step-by-step execution -This document details the complete workflow for running CodeQL analysis and orchestrating remediation. +This document details the complete workflow for running CodeQL analysis and remediating findings safely. ## Step 1: Repository inspection @@ -147,27 +147,49 @@ See [sarif-interpretation.md](sarif-interpretation.md) for detailed prioritizati ## Step 7: Create remediation plan -Before assigning work, create a concise remediation plan: +Before editing or assigning work, create a concise remediation plan: - Applicable languages found - CodeQL databases reused or created - Number of findings per language - Findings selected for remediation - Files likely affected -- Suggested task split for parallel coder agents +- Whether each finding should be fixed locally or split into a delegated task - Verification commands to run after changes Prefer small, isolated tasks. Avoid assigning multiple agents to edit the same file. -## Step 8: Delegate to parallel coder agents +If a finding is confined to one file or one local control path, prefer fixing it directly instead of opening a delegation loop. -Assign tasks to coder agents only after the plan is clear. Use the template in [../assets/task-delegate-template.md](../assets/task-delegate-template.md). +## Step 8: Fix locally or delegate selectively -Do not immediately edit code yourself unless the change is very small and does not benefit from delegation. +Choose the lightest-weight path that preserves reviewability: -## Step 9: Review changes +- **Fix locally** when one small edit and one focused validation can test the hypothesis +- **Delegate** when findings split cleanly across files, languages, or subsystems -After coder agents return their work: +If delegating, use the template in [../assets/task-delegate-template.md](../assets/task-delegate-template.md). + +Do not force delegation for narrow or obviously local findings. + +## Step 9: Validate each fix immediately + +After the first substantive edit in a slice, run the cheapest focused validation before doing more work on that slice. + +Prefer this order: +1. Narrow syntax or compile check for the touched file +2. Focused test or command that exercises the behavior +3. Narrow CodeQL rerun for the exact rule or language slice when a residual finding remains +4. Broader verification only after the local hypothesis holds + +Examples: +- Python: `python -m py_compile path/to/file.py` +- JavaScript: `node --check path/to/file.js` +- CodeQL single-query rerun for one remaining rule in one language + +## Step 10: Review changes + +After local edits or coder-agent work: 1. Inspect all diffs 2. Check whether each task actually addresses the finding @@ -178,7 +200,7 @@ After coder agents return their work: If a coder agent introduces risky or unrelated changes, revert or narrow them. -## Step 10: Reanalysis and verification +## Step 11: Reanalysis and verification After code changes, rerun CodeQL for affected languages. @@ -216,11 +238,19 @@ codeql database analyze .codeql/dbs/javascript-typescript \ --output=.codeql/results/codeql-javascript-typescript-after.sarif ``` +Write refreshed results to distinct files such as `*-after.sarif` so the baseline remains available for comparison. + **Compare before and after results** to confirm that targeted findings are resolved. +When comparing SARIF: +- Extract at least rule ID, file path, line number, and message +- Separate truly removed findings from line-number churn caused by edits +- Call out added findings explicitly instead of assuming they are regressions +- If only one residual finding remains, a narrow rerun may be cheaper and more informative than immediately repeating the full suite + Use language-specific verification commands from [verification-commands.md](verification-commands.md) to ensure the repository still works. -## Step 11: Final report +## Step 12: Final report Generate a concise report using [../assets/report-template.md](../assets/report-template.md). diff --git a/.copilot/skills/codeql-analysis-remediation/references/verification-commands.md b/.copilot/skills/codeql-analysis-remediation/references/verification-commands.md index b6653137..dd3f7cec 100644 --- a/.copilot/skills/codeql-analysis-remediation/references/verification-commands.md +++ b/.copilot/skills/codeql-analysis-remediation/references/verification-commands.md @@ -1,6 +1,13 @@ # Language-specific verification commands -After making code changes to fix CodeQL findings, run relevant verification commands to ensure the repository still works. +After making code changes to fix CodeQL findings, run the cheapest relevant verification commands first, then widen only if the local fix holds. + +For CodeQL remediation, verification usually happens in two layers: + +1. **Local slice validation** for the touched file or behavior +2. **Scan validation** by comparing before/after SARIF or rerunning a narrow query/full language suite as appropriate + +Do not jump straight to broad test suites when a cheap file-scoped check can falsify the change faster. ## Python verification @@ -33,10 +40,12 @@ Before running commands, check what tools and scripts the project supports. | `pre-commit run --all-files` | Run all pre-commit hooks directly | **Recommended order:** -1. `uv run pytest` (or `pytest`) โ€” Unit tests first -2. `uv run mypy .` (or `mypy .`) โ€” Type checking -3. `uv run ruff check . --fix` โ€” Linting and auto-fixes -4. `uv run black .` (or `black .`) โ€” Code formatting +1. `python -m py_compile path/to/file.py` โ€” Cheap syntax validation for touched files +2. Focused test command for the touched module or Django app +3. `uv run pytest` (or `pytest`) โ€” Broader unit tests when justified +4. `uv run mypy .` (or `mypy .`) โ€” Type checking when used by the project +5. `uv run ruff check .` โ€” Linting +6. `uv run pre-commit run --all-files` โ€” Broader repository validation when needed If any command fails: - Determine whether the failure is caused by your changes or was pre-existing @@ -91,10 +100,11 @@ Before running commands, inspect `package.json` to see available scripts. | `npm run format:check` | Check code formatting | **Recommended order:** -1. `npm run lint` (or `npm run lint:fix`) โ€” Linting -2. `npm run typecheck` โ€” TypeScript type checking -3. `npm test` โ€” Unit tests -4. `npm run build` โ€” Build (if applicable) +1. `node --check path/to/file.js` โ€” Cheap syntax validation for touched files +2. `npm run lint` (or `npm run lint:fix`) โ€” Linting +3. `npm run typecheck` โ€” TypeScript type checking +4. `npm test` โ€” Unit tests +5. `npm run build` โ€” Build (if applicable) If any command fails: - Determine whether the failure is caused by your changes or was pre-existing @@ -123,6 +133,27 @@ npm run build echo "โœ“ JavaScript/TypeScript verification complete" ``` +## Scan verification + +For CodeQL work, validation is incomplete until you verify the finding delta. + +Use one of these patterns: + +1. **Before/after SARIF compare** + - Keep the original SARIF as the baseline + - Write refreshed output to a distinct file such as `codeql-python-after.sarif` + - Compare rule ID, file, line, and message + - Treat line-only movement carefully; it may reflect the same finding after edits + +2. **Narrow rerun for a residual finding** + - Use when one rule or one remaining finding is under investigation + - Prefer this when a full reanalysis is expensive and the question is localized + +3. **Full language rerun** + - Use after substantial changes or when the baseline database is likely stale + +If a terminal repeatedly reports that a long CodeQL run "may be waiting for input," confirm from the output whether it is still making progress before treating it as blocked. + ## Repository state verification For any language, check basic repository health: @@ -136,7 +167,7 @@ For any language, check basic repository health: 2. **No syntax errors in primary files:** - Python: `python -m py_compile ` - - JavaScript: `node -c ` + - JavaScript: `node --check ` - TypeScript: `tsc --noEmit` 3. **No circular imports or module issues:** @@ -176,7 +207,7 @@ Some checks are optional depending on the project: Only run commands that are actually configured in the project. If in doubt, run: -- **Python:** `uv run pytest` -- **JavaScript/TypeScript:** `npm test` +- **Python:** `python -m py_compile ` and then `uv run pytest` +- **JavaScript/TypeScript:** `node --check ` and then `npm test` These are the most common, lowest-risk verification points. From 338f144771c377105d09e65f7b5a698dae1038bf Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 6 May 2026 12:03:19 +0000 Subject: [PATCH 138/199] Refactor audit-python-template-compliance skill: Update documentation, remove unused config files, and enhance asset management - Revised SKILL.md to clarify configuration file handling and asset sourcing. - Deleted obsolete configuration files from assets/config-template, including .detect-secrets.yaml, Dockerfile, and devcontainer.json. - Updated audit-procedure.md and template-config-settings.md to reflect changes in asset management and file sourcing. - Introduced config-template-manifest.txt to track repository paths materialized during installation. - Enhanced install-copilot-agents.sh to support materializing config assets from the repository. --- .../audit-python-template-compliance/SKILL.md | 22 +- .../assets/config-template-manifest.txt | 16 ++ .../config-template/.detect-secrets.yaml | 3 - .../config-template/.devcontainer/Dockerfile | 22 -- .../.devcontainer/devcontainer.json | 64 ------ .../.devcontainer/post-create.sh | 27 --- .../assets/config-template/.envrc | 15 -- .../assets/config-template/.gitattributes | 5 - .../config-template/.github/dependabot.yml | 34 --- .../config-template/.github/workflows/ci.yml | 90 -------- .../assets/config-template/.gitignore | 106 --------- .../config-template/.pre-commit-config.yaml | 120 ---------- .../config-template/.vscode/extensions.json | 30 --- .../config-template/.vscode/settings.json | 16 -- .../assets/config-template/pyproject.toml | 210 ------------------ .../scripts/hooks/nbstripout-autoadd.sh | 14 -- .../scripts/hooks/run-pip-audit.sh | 27 --- .../references/audit-procedure.md | 20 +- .../references/template-config-settings.md | 75 ++++--- scripts/install-copilot-agents.sh | 54 ++++- 20 files changed, 142 insertions(+), 828 deletions(-) create mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.detect-secrets.yaml delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/post-create.sh delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.envrc delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.gitattributes delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.github/dependabot.yml delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.github/workflows/ci.yml delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.gitignore delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/settings.json delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/pyproject.toml delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/nbstripout-autoadd.sh delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/run-pip-audit.sh diff --git a/.copilot/skills/audit-python-template-compliance/SKILL.md b/.copilot/skills/audit-python-template-compliance/SKILL.md index 1a59d308..d0abefea 100644 --- a/.copilot/skills/audit-python-template-compliance/SKILL.md +++ b/.copilot/skills/audit-python-template-compliance/SKILL.md @@ -31,10 +31,12 @@ available validation commands, and finish with a concise audit report. 1. Confirm the target repository and record any approved exceptions before changing files. 2. Treat executable configuration as the source of truth when prose docs drift. -3. For config-bearing files shipped with this skill, copy the provided file as - the base instead of recreating it. Prefer a matching literal file from this - skill directory first, then from `assets/config-template/`, and only after - that replace the approved target-specific values. +3. For config-bearing files indexed by this skill, copy the provided base file + instead of recreating it. Use files shipped directly with this skill when + they exist; otherwise use the repo-sourced assets materialized from + `assets/config-template-manifest.txt` into `assets/config-template/` at + install time, and only after that replace the approved target-specific + values. 4. Package names, repo URLs, excluded directories, cache volume names, and secret references must match the target repository. 5. Fix root causes first. Do not patch around validation failures with @@ -44,9 +46,9 @@ available validation commands, and finish with a concise audit report. - Read the template baseline in [template-baseline](./references/template-baseline.md). - Use the asset index in [template-config-settings](./references/template-config-settings.md) - as the default copy source for config files. When a matching shipped file - exists, copy it first and then make only the approved target-specific - substitutions. + as the default copy source for config files. When a matching shipped file or + materialized repo asset exists, copy it first and then make only the + approved target-specific substitutions. - Follow the execution sequence in [audit-procedure](./references/audit-procedure.md). - Use the final structure in [audit-report-template](./assets/audit-report-template.md). @@ -68,9 +70,9 @@ By the end of the run, produce all of the following when feasible: `.pre-commit-config.yaml`, `.github/workflows/`, `.devcontainer/`, `scripts/hooks/`, `README.md`, `docs/`, first-party packages, and test entry points. -- When a config surface has a shipped base file in this skill, copy that file - into place before making local adjustments. Do not rebuild those configs from - memory. +- When a config surface has a shipped base file or a materialized repo asset in + this skill, copy that file into place before making local adjustments. Do not + rebuild those configs from memory. - Keep the first edits small and validate immediately. - Parallelize only independent slices, such as docs versus CI copy edits, after you know they do not share the same controlling config. diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt b/.copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt new file mode 100644 index 00000000..1d4c5274 --- /dev/null +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt @@ -0,0 +1,16 @@ +# Relative repository paths materialized into assets/config-template/ at install time. +pyproject.toml +.pre-commit-config.yaml +.github/workflows/ci.yml +.github/dependabot.yml +.devcontainer/devcontainer.json +.devcontainer/Dockerfile +.devcontainer/post-create.sh +.vscode/extensions.json +.vscode/settings.json +.envrc +.detect-secrets.yaml +.gitattributes +.gitignore +scripts/hooks/nbstripout-autoadd.sh +scripts/hooks/run-pip-audit.sh diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.detect-secrets.yaml b/.copilot/skills/audit-python-template-compliance/assets/config-template/.detect-secrets.yaml deleted file mode 100644 index c73c9a1e..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.detect-secrets.yaml +++ /dev/null @@ -1,3 +0,0 @@ -exclude: - files: - - ^tests/ diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile deleted file mode 100644 index 61284dcc..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM mcr.microsoft.com/devcontainers/python:1-3.12-bookworm - -ARG USERNAME=vscode -ARG UV_VERSION=0.9.25 - -ADD https://astral.sh/uv/${UV_VERSION}/install.sh /tmp/uv-installer.sh - -RUN env UV_UNMANAGED_INSTALL="/usr/local/bin" sh /tmp/uv-installer.sh \ - && rm /tmp/uv-installer.sh - -RUN rm -f /etc/apt/sources.list.d/yarn.list \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ - bubblewrap \ - socat \ - && rm -rf /var/lib/apt/lists/* - -ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ - PYTHONDONTWRITEBYTECODE=1 - -RUN mkdir -p /home/${USERNAME}/.cache/uv /home/${USERNAME}/.cache/pre-commit \ - && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.cache diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json deleted file mode 100644 index 00eb7364..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/devcontainer.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "build": { - "context": "..", - "dockerfile": "Dockerfile" - }, - "containerEnv": { - "PRE_COMMIT_HOME": "/home/vscode/.cache/pre-commit", - "UV_CACHE_DIR": "/home/vscode/.cache/uv", - "UV_LINK_MODE": "copy", - "UV_PYTHON_DOWNLOADS": "never" - }, - "customizations": { - "vscode": { - "extensions": [ - "ms-vscode-remote.remote-containers", - "ms-azuretools.vscode-docker", - "ms-python.python", - "ms-python.vscode-pylance", - "ms-python.mypy-type-checker", - "charliermarsh.ruff", - "ms-toolsai.jupyter", - "astral-sh.ty", - "ms-python.autopep8", - "ms-python.black-formatter", - "ms-python.debugpy", - "mgesbert.python-path", - "docker.docker", - "dbaeumer.vscode-eslint", - "tamasfe.even-better-toml", - "cweijan.vscode-excel", - "GitHub.copilot-chat", - "github.vscode-github-actions", - "GitHub.vscode-pull-request-github", - "ms-azuretools.vscode-azure-github-copilot", - "mtxr.sqltools", - "mtxr.sqltools-driver-mysql", - "mtxr.sqltools-driver-pg", - "mtxr.sqltools-driver-sqlite", - "ms-ossdata.vscode-pgsql", - "formulahendry.docker-explorer" - ], - "settings": { - "jupyter.jupyterServerType": "local", - "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", - "python.terminal.activateEnvironment": true, - "python.testing.pytestArgs": [ - "." - ], - "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false, - "ruff.enable": true - } - } - }, - "mounts": [ - "source=your-repo-uv-cache,target=/home/vscode/.cache/uv,type=volume", - "source=your-repo-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume" - ], - "name": "your-project-name", - "postCreateCommand": "bash .devcontainer/post-create.sh", - "remoteUser": "vscode", - "updateRemoteUserUID": true, - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}" -} diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/post-create.sh b/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/post-create.sh deleted file mode 100644 index 6e517d41..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.devcontainer/post-create.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "${repo_root}" - -echo "Bootstrapping the container workspace with uv..." -uv --version -python --version - -uv sync --frozen - -if [[ -d .git ]]; then - uv run pre-commit install --install-hooks -fi - -if ! uv run python -c "import ipykernel" >/dev/null 2>&1; then - echo "Notebook execution in the container uses the local Python environment and needs ipykernel available there." -fi - -if command -v op >/dev/null 2>&1; then - echo "1Password CLI detected. Run 'op account list' if you need secret-backed examples." -else - echo "1Password CLI is optional and not installed in this container." -fi - -echo "For Windows contributors, keep this repo inside the WSL filesystem before reopening it in the container for the best bind-mount performance." diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.envrc b/.copilot/skills/audit-python-template-compliance/assets/config-template/.envrc deleted file mode 100644 index 1ee6f0cb..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.envrc +++ /dev/null @@ -1,15 +0,0 @@ -# Load environment variables from .env if it exists -if [ -f .env ]; then - set -a - source .env - set +a -fi - -export PYTHONPATH=. -export VENV_DIR=".venv" -export PYPROJECT="pyproject.toml" - -# Set a virtualenv automatically if venv exists -if [ -d "$VENV_DIR" ]; then - source $VENV_DIR/bin/activate -fi diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitattributes b/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitattributes deleted file mode 100644 index 88f68334..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitattributes +++ /dev/null @@ -1,5 +0,0 @@ -* text=auto -*.sh eol=lf -*.ps1 eol=crlf -*.py eol=lf -*.ipynb -text diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/dependabot.yml b/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/dependabot.yml deleted file mode 100644 index f013d7dd..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/dependabot.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: 2 - -updates: - # Python dependencies (uv project) - - package-ecosystem: "uv" - directory: "/" - schedule: - interval: "daily" - open-pull-requests-limit: 0 - - # Web dependencies via package.json (Bun-compatible through npm ecosystem) - # Remove or adjust this entry if the target repo does not ship a /web app. - - package-ecosystem: "npm" - directory: "/web" - schedule: - interval: "daily" - open-pull-requests-limit: 0 - commit-message: - prefix: "deps" - versioning-strategy: increase-if-necessary - - # Dockerfile base images - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "daily" - open-pull-requests-limit: 0 - - # GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - open-pull-requests-limit: 0 diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/workflows/ci.yml b/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/workflows/ci.yml deleted file mode 100644 index fce4094f..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.github/workflows/ci.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: CI - -"on": - pull_request: - branches: - - main - - staging - push: - branches: - - main - - staging - -permissions: - contents: write - -jobs: - lint-and-test: - name: Lint & Test - runs-on: ubuntu-latest - env: - PRE_COMMIT_HOME: ${{ github.workspace }}/.pre-commit-cache - strategy: - matrix: - python-version: ['3.12'] - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - persist-credentials: true - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - - - name: Install dependencies - run: uv sync - - - name: Cache pre-commit hooks - uses: actions/cache@v4 - with: - path: ${{ github.workspace }}/.pre-commit-cache - key: pre-commit-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - pre-commit-${{ runner.os }}-${{ matrix.python-version }}- - - - name: Run pre-commit hooks - run: uv run pre-commit run --all-files - - - name: Run tests with coverage - run: uv run pytest --cov --cov-report=term --cov-report=xml - - - name: Generate coverage badge - if: github.event_name == 'pull_request' - run: uv run genbadge coverage -i coverage.xml -o coverage.svg - - - name: Commit badge - uses: stefanzweifel/git-auto-commit-action@v7 - if: github.event_name == 'pull_request' - with: - commit_message: "chore: update coverage badge" - file_pattern: coverage.svg - branch: ${{ github.head_ref }} - - devcontainer-smoke: - name: Devcontainer Smoke - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and bootstrap devcontainer - uses: devcontainers/ci@v0.3 - with: - configFile: .devcontainer/devcontainer.json - push: never - runCmd: | - test -d .venv - test -x .git/hooks/pre-commit - uv --version - .venv/bin/python --version diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitignore b/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitignore deleted file mode 100644 index 48a2be52..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.gitignore +++ /dev/null @@ -1,106 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# Virtual environments -.venv/ -venv/ -env/ -ENV/ - -# CSV and Excel files -*.csv -*.xlsx -*.xls -*.xlsm -*.xlsb -*.ods - -# Jupyter Notebook checkpoints -.ipynb_checkpoints/ - -# VSCode settings -# .vscode/ - -# PyCharm settings -.idea/ - -# macOS files -.DS_Store - -# mypy type checker cache -.mypy_cache/ -.dmypy.json -dmypy.json - -.ruff_cache/ - -# Pyre type checker -.pyre/ - -# pytype -.pytype/ - -# Pytest cache -.pytest_cache/ - -# Coverage reports -htmlcov/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml - -# Test output -test-results/ - -# Build / distribution -build/ -dist/ -*.egg-info/ -.eggs/ -*.egg - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage tools -.tox/ -.nox/ - -# Python wheels -*.whl - -# Editable installs -*.egg-link - -# dotenv files (if used for secrets) -.env -.env.* - -# Poetry -poetry.lock - -# PDM -__pypackages__/ - -# PyInstaller -*.spec - -# Log files -*.log - -# Temporary files -*.bak -*.swp -*.swo -*~ - -# Local notebooks -notebooks-local/ - -# direnv -.direnv/ diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml b/.copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml deleted file mode 100644 index ba3f7224..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.pre-commit-config.yaml +++ /dev/null @@ -1,120 +0,0 @@ -repos: - - repo: local - hooks: - # Strip notebook outputs - - id: nbstripout-autoadd - name: nbstripout (auto-add notebooks) - entry: uv run scripts/hooks/nbstripout-autoadd.sh - language: system - types: [jupyter] - - # Python formatter - - repo: https://github.com/psf/black - rev: 25.9.0 - hooks: - - id: black - name: Black (python formatter) - language_version: python3.12 - types: [python] - - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - exclude: coverage\.svg$ - - id: check-toml - - id: check-json - - id: check-symlinks - - id: check-yaml - - id: check-added-large-files - - id: check-merge-conflict - - id: detect-private-key - - id: pretty-format-json - args: ["--autofix"] - exclude: \.ipynb$ - - # Dependency vulnerability scanning with uv.lock - - repo: https://github.com/pypa/pip-audit - rev: v2.10.0 - hooks: - - id: pip-audit - args: ["--progress-spinner", "off", "--desc"] - entry: uv run bash scripts/hooks/run-pip-audit.sh - language: system - stages: [pre-commit] - - # Secrets scanning - - repo: https://github.com/Yelp/detect-secrets - rev: v1.5.0 - hooks: - - id: detect-secrets - args: ["--baseline", ".secret.baseline"] - exclude: ^tests/ - - # Notebook/text sync with jupytext - - repo: https://github.com/mwouts/jupytext - rev: v1.19.1 - hooks: - - id: jupytext - args: [--sync] - require_serial: true - - # Type checking with mypy - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 - hooks: - - id: mypy - name: Type check with mypy - args: [ "--config-file=pyproject.toml", "--install-types", "--non-interactive" ] - pass_filenames: false - - # Modernize Python syntax - - repo: https://github.com/asottile/pyupgrade - rev: v3.21.2 - hooks: - - id: pyupgrade - name: pyupgrade (modernize Python syntax) - args: [--py312-plus] - types: [python] - - # Linting and formatting with ruff - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.4 - hooks: - - id: ruff - name: Ruff (linting + import sorting + formatting) - args: [--fix] - types: [python] - - # Security scanning - - repo: https://github.com/PyCQA/bandit - rev: 1.9.4 - hooks: - - id: bandit - name: bandit (security check) - args: [-ll, --skip, B101] - exclude: ^tests/ - types: [python] - - # Docstring validation - - repo: https://github.com/PyCQA/pydocstyle - rev: 6.3.0 - hooks: - - id: pydocstyle - name: pydocstyle (docstring validation) - # Adjust this regex for the target repo's non-production directories. - exclude: ^(tests/|example/) - types: [python] - - # Testing with pytest (locally managed by uv) - - repo: local - hooks: - - id: pytest - name: Run pytest with coverage - # Replace the beartype package list for the target repo when needed. - entry: uv run pytest --cov --cov-fail-under=80 --beartype-packages=your_package - language: system - pass_filenames: false - types: [python] - stages: [pre-commit] diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json b/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json deleted file mode 100644 index 4355965e..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/extensions.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "recommendations": [ - "ms-vscode-remote.remote-containers", - "ms-azuretools.vscode-docker", - "ms-python.python", - "ms-python.vscode-pylance", - "ms-python.mypy-type-checker", - "charliermarsh.ruff", - "ms-toolsai.jupyter", - "astral-sh.ty", - "ms-python.autopep8", - "ms-python.black-formatter", - "ms-python.debugpy", - "mgesbert.python-path", - "docker.docker", - "dbaeumer.vscode-eslint", - "tamasfe.even-better-toml", - "cweijan.vscode-excel", - "GitHub.copilot-chat", - "github.vscode-github-actions", - "GitHub.vscode-pull-request-github", - "ms-azuretools.vscode-azure-github-copilot", - "mtxr.sqltools", - "mtxr.sqltools-driver-mysql", - "mtxr.sqltools-driver-pg", - "mtxr.sqltools-driver-sqlite", - "ms-ossdata.vscode-pgsql", - "formulahendry.docker-explorer" - ] -} diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/settings.json b/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/settings.json deleted file mode 100644 index e6b2045f..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/.vscode/settings.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "mypy-type-checker.args": [ - "--config-file=pyproject.toml" - ], - "mypy-type-checker.cwd": "${workspaceFolder}", - "mypy-type-checker.importStrategy": "fromEnvironment", - "mypy-type-checker.reportingScope": "workspace", - "python.analysis.typeCheckingMode": "standard", - "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", - "python.testing.pytestArgs": [ - "." - ], - "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false, - "ruff.enable": true -} diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/pyproject.toml b/.copilot/skills/audit-python-template-compliance/assets/config-template/pyproject.toml deleted file mode 100644 index 70c38352..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/pyproject.toml +++ /dev/null @@ -1,210 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0", "wheel", "pip>=26.0,<27.0.0", "uv>=0.9.25,<1.0.0"] -build-backend = "setuptools.build_meta" - -[project.urls] -# Replace with the target repository URLs. -Homepage = "https://github.com/TECHLETES/your-repo" -Repository = "https://github.com/TECHLETES/your-repo" -Issues = "https://github.com/TECHLETES/your-repo/issues" - -[project] -# Replace project metadata for the target repository. -name = "your-project-name" -version = "0.1.0" -description = "Your project description." -readme = "README.md" -requires-python = ">=3.12,<4.0" -keywords = ["devops", "template", "python"] -authors = [{ name = "Your Name", email = "you@example.com" }] -maintainers = [{ name = "Your Team", email = "team@example.com" }] -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", -] - -# Core production dependencies required for the application to run. -# Pinned with >= and < to allow patch/minor updates while preventing major version breaks. -# Add only dependencies that are needed for the app to run in production. -dependencies = [ - # Add the target repo's runtime dependencies here. -] - -# Optional dependency groups for different environments and use cases. -# Managed by uv (PEP 735 format) and locked in uv.lock for reproducible builds. -# Install with: uv sync (all groups), uv sync --no-group dev (no dev), uv sync --only-group extra (extra only), etc. -[dependency-groups] -# Development and testing dependencies. -# Includes code quality tools, testing frameworks, and development utilities. -# Install with: uv sync (default includes dev) or uv sync --no-group dev to skip -dev = [ - "pre-commit>=4.5.0,<5.0.0", - "detect-secrets>=1.5.0,<2.0.0", - "nbstripout>=0.8.0,<1.0.0", - "ipykernel>=6.29.0,<7.0.0", - "pytest>=9.0.3,<10.0.0", - "pytest-cov>=7.0.0,<8.0.0", - "black>=25.12.0,<27.0.0", - "mypy>=1.19.0,<2.0.0", - "ruff>=0.14.0,<1.0.0", - "beartype>=0.22.0,<1.0.0", - "pytest-beartype>=0.2.0,<1.0.0", - "pip-audit>=2.10.0,<3.0.0", - "rich>=14.2.0,<15.0.0", - "pyupgrade>=3.15.0,<4.0.0", - "codespell>=2.2.0,<3.0.0", - "bandit>=1.7.0,<2.0.0", - "pydocstyle>=6.3.0,<7.0.0", - "genbadge[coverage]>=1.1.0,<2.0.0", - "testcontainers>=4.8.0", - "pillow~=12.2" -] - -# Additional production features and optional components. -# Install with: uv sync --group extra or uv sync --no-group extra -extra = [ - # Add optional production dependencies for the target repo here. -] - -[tool.setuptools.packages.find] -where = ["."] -include = [ - # Replace with the target repo's importable packages, e.g. "your_package*". -] -exclude = ["tests*"] - -[tool.black] -line-length = 88 -target-version = ["py312"] - -[tool.pytest] -minversion = "9.0.3" -addopts = [ - "--strict-markers", - "--strict-config", - # Replace with the target repo's coverage package arguments. - # "--cov=your_package", - "--cov-report=term-missing", - "--cov-report=html", - "--cov-report=xml", - # Replace with the target repo's beartype-enabled packages when used. - # "--beartype-packages=your_package", -] -testpaths = ["tests"] -python_files = ["test_*.py", "*_test.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] -pythonpath = ["."] - -[tool.coverage.run] -source = [ - # Replace with the target repo's production packages. - # "your_package", -] -omit = [ - "*/__pycache__/*", - "*/tests/*", - "*/.venv/*", - "*/venv/*", - "*/__init__.py", - "setup.py", - # Add any other files you want to exclude from test coverage. -] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "def __repr__", - "if self.debug:", - "if settings.DEBUG", - "raise AssertionError", - "raise NotImplementedError", - "if 0:", - "if __name__ == .__main__.:", - "class .*\\bProtocol\\):", - "@(abc\\.)?abstractmethod", - "except ImportError", - "pass", -] - -[tool.mypy] -python_version = "3.12" - -# ignore_missing_imports for third-party libs without type hints -ignore_missing_imports = true -follow_imports = "silent" - -# strictness settings - adjust as needed -warn_unused_ignores = true -warn_return_any = true -warn_unreachable = true -no_implicit_optional = true -allow_redefinition = true -show_column_numbers = true -pretty = true -warn_unused_configs = true - -# enforce typing on own code -disallow_untyped_defs = true -disallow_incomplete_defs = true - -# Specify which folders/files to type check. -files = [ - # Replace with the target repo's packages or entry points to type check. - # "your_package", -] - -exclude = ''' -(?x)( - # Adjust these patterns for the target repo. - ^migrations/ - | ^notebooks/ - | ^tests/ - | ^\.venv/ - | ^venv/ -) -''' -plugins = [] - -[tool.ruff] -target-version = "py312" -line-length = 88 -fix = true -respect-gitignore = true -extend-exclude = [ - # Add project-specific exclusions here, e.g. "migrations". -] -lint.dummy-variable-rgx = "^_|^__|^pytest$" - -# Enable popular rule sets -lint.select = [ - "E", # pycodestyle - "F", # pyflakes - "I", # import sorting - "B", # bugbear - "UP", # pyupgrade -] - -# Rules to ignore (common ones) -lint.ignore = [ - "B008", # Do not perform function calls in argument defaults - "E501", # Line too long (Black handles this) -] - -[tool.bandit] -exclude_dirs = ["tests", ".venv"] -skips = ["B101", "B601"] - -[tool.pip-audit] -# Centralized CVE/PYSEC exclusions for pip-audit -# Used by both pre-commit hooks and CI workflows via scripts/hooks/run-pip-audit.sh -# See docs/6_pre_commit_troubleshooting.md for explanation of each exclusion -ignore = [ - "CVE-2026-1703", # Path traversal in pip when extracting malicious wheels -] - -[tool.uv] -required-version = ">=0.9.25,<1.0.0" diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/nbstripout-autoadd.sh b/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/nbstripout-autoadd.sh deleted file mode 100644 index 8220c4ab..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/nbstripout-autoadd.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -e - -# Run nbstripout on all files passed by pre-commit -nbstripout "$@" - -# Re-add any modified .ipynb files to the index -modified=$(git ls-files -m '*.ipynb') || true - -if [ -n "$modified" ]; then - echo "๐Ÿ” Re-staging modified notebooks:" - echo "$modified" - git add $modified -fi diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/run-pip-audit.sh b/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/run-pip-audit.sh deleted file mode 100644 index 0202b037..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template/scripts/hooks/run-pip-audit.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# Helper script to build pip-audit ignore arguments from pyproject.toml [tool.pip-audit] -# This ensures both pre-commit and CI use the same CVE exclusion list - -# Extract ignore list from pyproject.toml using Python -IGNORE_ARGS=$(python3 << 'EOF' -import tomllib -import sys - -try: - with open("pyproject.toml", "rb") as f: - config = tomllib.load(f) - - # Get the ignore list from [tool.pip-audit] - ignore_list = config.get("tool", {}).get("pip-audit", {}).get("ignore", []) - - # Build the arguments - args = " ".join([f"--ignore-vuln {cve}" for cve in ignore_list]) - print(args) -except Exception as e: - print(f"Error reading pyproject.toml: {e}", file=sys.stderr) - sys.exit(1) -EOF -) - -# Run pip-audit with the exclusions from pyproject.toml -pip-audit $IGNORE_ARGS "$@" diff --git a/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md b/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md index ce82ead0..2bfef638 100644 --- a/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md +++ b/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md @@ -55,11 +55,12 @@ Primary evidence set: `scripts/hooks/`, and audit wrappers - Copilot assets under `.copilot/` when present -Use `template-config-settings.md` and its shipped config files as the default -base for concrete tool settings such as pytest `addopts`, mypy strictness -flags, Ruff rule sets, Bandit skips, pre-commit hook revisions, delegated hook -scripts, and devcontainer editor settings. When a matching shipped file exists, -copy it first and then replace only the approved target-specific values. +Use `template-config-settings.md` and its shipped or materialized config files +as the default base for concrete tool settings such as pytest `addopts`, mypy +strictness flags, Ruff rule sets, Bandit skips, pre-commit hook revisions, +delegated hook scripts, and devcontainer editor settings. When a matching +shipped file or materialized repo asset exists, copy it first and then replace +only the approved target-specific values. If a baseline file is missing, treat that as a finding rather than continuing as though the capability exists. @@ -123,16 +124,17 @@ baseline. Rules: 1. For shipped config surfaces, copy the matching file from this skill as the - base before editing. Prefer files shipped directly with the skill, then - `assets/config-template/`. + base before editing. Prefer files shipped directly with the skill, then the + repo-sourced files materialized under `assets/config-template/` from + `assets/config-template-manifest.txt`. 2. Prefer adding missing baseline capabilities over documenting gaps away. 3. If the target repo already has an equivalent mechanism, align and document it instead of replacing it unnecessarily. 4. Keep edits reversible and validate the touched slice immediately. For missing or substantially divergent config files, do not rebuild them from -memory. Start from the shipped base file, then apply target-specific -substitutions. +memory. Start from the shipped or materialized base file, then apply +target-specific substitutions. ### Focused Validation After Each Edit Slice diff --git a/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md b/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md index a4372ce0..511a7209 100644 --- a/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md +++ b/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md @@ -1,13 +1,15 @@ # Template Config Assets -This reference acts as the copy-first index for config files shipped with this +This reference acts as the copy-first index for config files provided by this skill. When a target repository needs one of these config surfaces, copy the matching -shipped file as the base instead of recreating the file by hand. Prefer a -matching literal file shipped directly in this skill directory when one exists; -otherwise copy the file from `assets/config-template/`. After copying, replace -only the approved target-specific values. +base file instead of recreating the file by hand. Prefer a matching literal +file shipped directly in this skill directory when one exists. Otherwise use +the repo-sourced assets listed in +`../assets/config-template-manifest.txt`, which the installer materializes into +`../assets/config-template/`. After copying, replace only the approved +target-specific values. ## Generalization Rules @@ -21,54 +23,77 @@ be generalized after the copy step: repo rather than this template's `utils` and `example` packages - repo-specific cache volume names or similar identifiers -If an asset file and the current repository diverge, the repository file is the -source of truth and the asset should be updated. +If a listed asset and the current repository diverge, the repository file is +the source of truth and the manifest or materialization logic should be +updated. Do not synthesize these files from memory or rewrite them section-by-section when a shipped base file exists. +## Repo Asset Manifest + +The installer copies these repository paths into `assets/config-template/` for +this skill at install time: + +- `pyproject.toml` +- `.pre-commit-config.yaml` +- `.github/workflows/ci.yml` +- `.github/dependabot.yml` +- `.devcontainer/devcontainer.json` +- `.devcontainer/Dockerfile` +- `.devcontainer/post-create.sh` +- `.vscode/extensions.json` +- `.vscode/settings.json` +- `.envrc` +- `.detect-secrets.yaml` +- `.gitattributes` +- `.gitignore` +- `scripts/hooks/nbstripout-autoadd.sh` +- `scripts/hooks/run-pip-audit.sh` + ## Asset Files ### Core Python Tooling -- [pyproject.toml literal template](../assets/config-template/pyproject.toml) -- [.pre-commit-config.yaml literal template](../assets/config-template/.pre-commit-config.yaml) +- `../assets/config-template/pyproject.toml` +- `../assets/config-template/.pre-commit-config.yaml` ### GitHub Automation -- [CI workflow template](../assets/config-template/.github/workflows/ci.yml) -- [Dependabot template](../assets/config-template/.github/dependabot.yml) +- `../assets/config-template/.github/workflows/ci.yml` +- `../assets/config-template/.github/dependabot.yml` ### Devcontainer -- [devcontainer.json template](../assets/config-template/.devcontainer/devcontainer.json) -- [Dockerfile template](../assets/config-template/.devcontainer/Dockerfile) -- [post-create.sh template](../assets/config-template/.devcontainer/post-create.sh) +- `../assets/config-template/.devcontainer/devcontainer.json` +- `../assets/config-template/.devcontainer/Dockerfile` +- `../assets/config-template/.devcontainer/post-create.sh` ### VS Code Workspace -- [extensions.json template](../assets/config-template/.vscode/extensions.json) -- [settings.json template](../assets/config-template/.vscode/settings.json) +- `../assets/config-template/.vscode/extensions.json` +- `../assets/config-template/.vscode/settings.json` ### Shell and Repo Hygiene -- [.envrc template](../assets/config-template/.envrc) -- [.detect-secrets.yaml template](../assets/config-template/.detect-secrets.yaml) -- [.gitattributes template](../assets/config-template/.gitattributes) -- [.gitignore template](../assets/config-template/.gitignore) +- `../assets/config-template/.envrc` +- `../assets/config-template/.detect-secrets.yaml` +- `../assets/config-template/.gitattributes` +- `../assets/config-template/.gitignore` ### Hook Scripts -- [nbstripout-autoadd.sh template](../assets/config-template/scripts/hooks/nbstripout-autoadd.sh) -- [run-pip-audit.sh template](../assets/config-template/scripts/hooks/run-pip-audit.sh) +- `../assets/config-template/scripts/hooks/nbstripout-autoadd.sh` +- `../assets/config-template/scripts/hooks/run-pip-audit.sh` ## How To Use During Audits 1. Identify whether the needed config file is shipped directly with this skill - or under `assets/config-template/`, then copy that file as the base. + or materialized under `assets/config-template/`, then copy that file as the + base. 2. Replace only the approved target-specific values and placeholders after the copy step. 3. Record findings when the target repo still diverges from the template's tooling, workflow, or structure beyond those approved generalization points. -4. Fall back to the live repository files only when this skill does not yet - ship a base file for the config surface you need. +4. Update `assets/config-template-manifest.txt` when a new repo-backed config + surface should be included for future audits. diff --git a/scripts/install-copilot-agents.sh b/scripts/install-copilot-agents.sh index d4a130a3..bd16d175 100755 --- a/scripts/install-copilot-agents.sh +++ b/scripts/install-copilot-agents.sh @@ -6,6 +6,9 @@ mode="copy" force="true" install_agents="true" install_skills="true" +audit_skill_name="audit-python-template-compliance" +audit_skill_manifest="assets/config-template-manifest.txt" +audit_skill_generated_dir="assets/config-template" usage() { cat <<'EOF' @@ -128,6 +131,53 @@ install_copilot_component() { return 0 } +install_audit_template_skill() { + local skill_dir="$1" + local target_skill_dir="$2" + local mode_arg="$3" + local manifest_path="${skill_dir}/${audit_skill_manifest}" + local generated_dir="${target_skill_dir}/${audit_skill_generated_dir}" + + if [[ ! -f "${manifest_path}" ]]; then + echo " Missing ${audit_skill_manifest} for ${audit_skill_name}" >&2 + return 1 + fi + + if [[ "${mode_arg}" == "link" ]]; then + echo " ${audit_skill_name}: link mode falls back to copy so repo config assets can be materialized" + fi + + mkdir -p "${target_skill_dir}" + cp -r "${skill_dir}/." "${target_skill_dir}" + rm -rf "${generated_dir}" + mkdir -p "${generated_dir}" + + local copied_assets=0 + + while IFS= read -r relative_path || [[ -n "${relative_path}" ]]; do + if [[ -z "${relative_path}" || "${relative_path}" =~ ^# ]]; then + continue + fi + + local source_path="${repo_root}/${relative_path}" + local target_path="${generated_dir}/${relative_path}" + + if [[ ! -e "${source_path}" ]]; then + echo " Missing repo asset for ${audit_skill_name}: ${relative_path}" >&2 + return 1 + fi + + mkdir -p "$(dirname "${target_path}")" + cp -r "${source_path}" "${target_path}" + copied_assets=$((copied_assets + 1)) + done < "${manifest_path}" + + echo " Copied ${audit_skill_name}/" + echo " Materialized ${copied_assets} repo config asset(s)" + + return 0 +} + # Install agents if [[ "${install_agents}" == "true" ]]; then echo "Installing Copilot Agents..." @@ -176,7 +226,9 @@ install_skills_function() { fi fi - if [[ "${mode_arg}" == "link" ]]; then + if [[ "${skill_name}" == "${audit_skill_name}" ]]; then + install_audit_template_skill "${skill_dir}" "${target_skill_dir}" "${mode_arg}" + elif [[ "${mode_arg}" == "link" ]]; then ln -s "${skill_dir}" "${target_skill_dir}" echo " Linked ${skill_name}/" else From 40ff8e0321552daa9ad36b6daecf675efe7645a6 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 6 May 2026 12:06:15 +0000 Subject: [PATCH 139/199] feat: update config template manifest and documentation references; remove deprecated scripts --- .../assets/config-template-manifest.txt | 7 + .../references/template-config-settings.md | 7 + scripts/new-branch.sh | 44 --- scripts/setup.sh | 280 ------------------ 4 files changed, 14 insertions(+), 324 deletions(-) delete mode 100755 scripts/new-branch.sh delete mode 100755 scripts/setup.sh diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt b/.copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt index 1d4c5274..b7ea4f1f 100644 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt +++ b/.copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt @@ -3,9 +3,16 @@ pyproject.toml .pre-commit-config.yaml .github/workflows/ci.yml .github/dependabot.yml +.github/pull_request_template.md +.github/copilot-instructions.md +.github/ISSUE_TEMPLATE/config.yml +.github/ISSUE_TEMPLATE/bug-report.md +.github/ISSUE_TEMPLATE/docs_update.md +.github/ISSUE_TEMPLATE/feature-request.md .devcontainer/devcontainer.json .devcontainer/Dockerfile .devcontainer/post-create.sh +.devcontainer/.dockerignore .vscode/extensions.json .vscode/settings.json .envrc diff --git a/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md b/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md index 511a7209..04ad7c04 100644 --- a/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md +++ b/.copilot/skills/audit-python-template-compliance/references/template-config-settings.md @@ -62,12 +62,19 @@ this skill at install time: - `../assets/config-template/.github/workflows/ci.yml` - `../assets/config-template/.github/dependabot.yml` +- `../assets/config-template/.github/pull_request_template.md` +- `../assets/config-template/.github/copilot-instructions.md` +- `../assets/config-template/.github/ISSUE_TEMPLATE/config.yml` +- `../assets/config-template/.github/ISSUE_TEMPLATE/bug-report.md` +- `../assets/config-template/.github/ISSUE_TEMPLATE/docs_update.md` +- `../assets/config-template/.github/ISSUE_TEMPLATE/feature-request.md` ### Devcontainer - `../assets/config-template/.devcontainer/devcontainer.json` - `../assets/config-template/.devcontainer/Dockerfile` - `../assets/config-template/.devcontainer/post-create.sh` +- `../assets/config-template/.devcontainer/.dockerignore` ### VS Code Workspace diff --git a/scripts/new-branch.sh b/scripts/new-branch.sh deleted file mode 100755 index c8f95b49..00000000 --- a/scripts/new-branch.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# Quick script to start a new feature branch -# Usage: ./scripts/new-feature.sh feature "responsive navigation" -# Usage: ./scripts/new-feature.sh fix "sidebar mobile issue" - -if [ $# -ne 2 ]; then - echo "Usage: $0 " - echo "Types: feature, fix, docs, ui, devops" - echo "Example: $0 feature 'add user dashboard'" - exit 1 -fi - -TYPE=$1 -DESCRIPTION=$2 - -# Convert description to branch-safe format -BRANCH_NAME=$(echo "$DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') - -FULL_BRANCH="${TYPE}/${BRANCH_NAME}" - -echo "cleaning up old branches" -# Prune remote-tracking branches (remove references to remote branches deleted on GitHub) - -git remote prune origin -echo "Pruned remote-tracking branches" - -# (Optional) Delete all local branches that have been merged into main (except main itself) -(git branch --merged main | grep -v "main" | xargs -n 1 git branch -d) || true -echo "Removed merged local branches" - -echo "Creating branch: $FULL_BRANCH" - - -# Create and switch to new branch from current HEAD (preserves local changes) -git checkout -b "$FULL_BRANCH" - -echo "โœ… Ready to work on: $FULL_BRANCH" -echo "" -echo "When you're done:" -echo "1. git add ." -echo "2. git commit -m \"${TYPE}: ${DESCRIPTION}\"" -echo "3. git push origin $FULL_BRANCH" -echo "4. Create PR on GitHub" diff --git a/scripts/setup.sh b/scripts/setup.sh deleted file mode 100755 index 9f4d60a6..00000000 --- a/scripts/setup.sh +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# configuration -export VENV_DIR="${VENV_DIR:-.venv}" -export PYPROJECT="${PYPROJECT:-pyproject.toml}" - -# ui helpers -GREEN='\033[0;32m' -CYAN='\033[1;36m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -BLUE='\033[0;34m' -RESET='\033[0m' -step() { echo -e "${CYAN}๐Ÿ‘‰ $1${RESET}"; } -success(){ echo -e "${GREEN}โœ… $1${RESET}"; } -warn() { echo -e "${YELLOW}โš  $1${RESET}"; } -error() { echo -e "${RED}โŒ $1${RESET}"; exit 1; } -info() { echo -e "${BLUE}โ„น $1${RESET}"; } -section(){ - echo -e "\n${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" - echo -e "${BLUE}๐Ÿ”ง $1${RESET}" - echo -e "${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" -} - -is_wsl() { - grep -qi "microsoft" /proc/sys/kernel/osrelease 2>/dev/null || [[ -n "${WSL_DISTRO_NAME:-}" ]] -} - -has_cmd() { command -v "$1" >/dev/null 2>&1; } - -# Universal package installation function -APT_UPDATED="" -install_package() { - local package_name=$1 - step "Attempting to install $package_name..." - if has_cmd apt-get; then - if [[ -z "${APT_UPDATED}" ]]; then - sudo apt-get update - APT_UPDATED="1" - fi - sudo apt-get install -y "$package_name" - elif has_cmd dnf; then - sudo dnf install -y "$package_name" - elif has_cmd yum; then - sudo yum install -y "$package_name" - else - error "Could not find apt-get, dnf, or yum. Please install '$package_name' manually." - return 1 - fi - success "$package_name installed successfully." -} - - -check_system_requirements() { - section "system requirements check" - step "verifying required system packages" - has_cmd python3 || error "Python 3 is not installed. Install Python 3.12+." - success "Python $(python3 --version | cut -d' ' -f2) found." - - # Check for pip and install if missing - if has_cmd pip; then - success "pip $(pip --version | cut -d' ' -f2) found." - else - warn "pip not found." - install_package "python3-pip" - fi - - has_cmd git || error "git is not installed." - success "git $(git --version | cut -d' ' -f3) found." - - step "verifying uv package manager" - if has_cmd uv; then - success "uv $(uv --version) found." - else - warn "uv not found. installing..." - if has_cmd curl; then - curl -LsSf https://astral.sh/uv/install.sh | sh - # Add uv to PATH for current session - export PATH="$HOME/.local/bin:$PATH" - success "uv installed successfully. Added to PATH." - else - error "curl is required to install uv. Please install curl or uv manually." - fi - fi - - if has_cmd direnv; then - success "direnv found." - else - warn "direnv not found." - install_package "direnv" - fi - - if has_cmd curl; then success "curl available."; else warn "curl not found. Some optional steps may be skipped."; fi - - section "docker setup" - step "checking for Docker and Docker Compose" - if has_cmd apt-get; then - if ! command -v docker &> /dev/null; then - step "Docker not found. Installing Docker..." - - # Remove old versions - sudo apt-get remove -y docker docker-engine docker.io containerd runc || true - - # Update packages - sudo apt-get update -y - sudo apt-get install -y ca-certificates curl gnupg lsb-release - - # Add Dockerโ€™s official GPG key - sudo mkdir -p /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg - - # Set up the Docker repository - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ - https://download.docker.com/linux/ubuntu \ - $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - - # Install Docker - sudo apt-get update -y - sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - - # Enable & start Docker - sudo systemctl enable docker - sudo systemctl start docker - - success "Docker installed successfully." - else - success "Docker is already installed." - fi - - # Check & install Docker Compose (v2 CLI plugin) - if ! docker compose version &> /dev/null; then - step "Docker Compose plugin not found. Installing..." - sudo apt-get install -y docker-compose-plugin - success "Docker Compose installed successfully." - else - success "Docker Compose is already installed." - fi - - # Verification - step "verifying installation" - docker --version - docker compose version - - success "Docker and Docker Compose are ready to use!" - else - warn "Docker installation only supported on Debian/Ubuntu systems with apt-get." - fi -} - -setup_dependencies() { - section "dependency management with uv" - info "managing dependencies from $PYPROJECT" - step "locking dependencies with uv" - uv lock - success "uv.lock created" - - step "syncing dependencies with uv" - uv sync - success "dependencies installed from uv.lock" -} - -configure_locale() { - section "system locale configuration" - step "ensuring locales" - if has_cmd apt-get; then - sudo apt-get install -y locales - sudo locale-gen en_US.UTF-8 - sudo update-locale LANG=en_US.UTF-8 - success "locales configured for Debian family" - elif has_cmd dnf || has_cmd yum; then - # On Fedora and RHEL locales are typically available. Configure via localectl. - if has_cmd localectl; then - sudo localectl set-locale LANG=en_US.UTF-8 - success "locales configured via localectl" - else - warn "localectl not found. verify locale manually" - fi - else - warn "no known package manager detected for locale configuration" - fi -} - -setup_direnv() { - section "direnv setup" - step "configuring direnv shell integration" - PROFILE="${HOME}/.bashrc" - if ! grep -q 'direnv hook bash' "$PROFILE"; then - echo 'eval "$(direnv hook bash)"' >> "$PROFILE" - success "added direnv hook to $PROFILE" - else - warn "direnv hook already present in $PROFILE" - fi - step "enabling direnv for this project" - if [ -f ".envrc" ]; then - direnv allow || true - success "direnv enabled for this project" - else - warn ".envrc not found. skipping allow" - fi -} - -setup_pre_commit() { - section "pre commit hooks setup" - step "installing pre commit hooks" - if [[ -f ".pre-commit-config.yaml" ]]; then - $VENV_DIR/bin/pre-commit install - success "pre commit hooks installed" - step "updating pre commit repos" - - $VENV_DIR/bin/pre-commit autoupdate - success "pre commit hooks updated" - step "running pre commit on all files" - $VENV_DIR/bin/pre-commit run --all-files || warn "some pre commit checks failed" - else - warn ".pre-commit-config.yaml not found. skipping" - fi -} - -setup_secrets() { - section "secret management setup" - - step "initializing secrets baseline" - if [ -f ".secret.baseline" ]; then - warn "existing .secret.baseline found. backing up" - cp .secret.baseline .secret.baseline.bak - fi - $VENV_DIR/bin/detect-secrets scan > .secret.baseline - success "secrets baseline created" - $VENV_DIR/bin/detect-secrets audit .secret.baseline || warn "manual review of baseline recommended" - success "secrets configuration verified" -} - -validate_project() { - section "project validation" - step "running final project validation" - info "checking imports" - - "$VENV_DIR/bin/python" - <<'PYCODE' -import importlib -mods = ["utils.secrets"] -for m in mods: - importlib.import_module(m) -print("โœ… All project modules import successfully") -PYCODE -} - -print_completion() { - section "setup complete" - echo -e "\n${GREEN}๐ŸŽ‰ development environment setup complete${RESET}\n" - echo -e "${BLUE}installed:${RESET}" - echo " โ€ข $VENV_DIR/" - echo " โ€ข uv.lock (all dependencies locked and synced)" - echo " โ€ข pre commit hooks" - echo " โ€ข secret baseline (.secret.baseline)" - echo " โ€ข direnv" - echo -e "\n${BLUE}next steps:${RESET}" - echo " 1. restart terminal or run: source ~/.bashrc" - echo " 2. test with: python example/using_secrets.py" - echo " 3. start coding" - echo -e "\n${BLUE}common commands:${RESET}" - echo " โ€ข uv sync - sync dependencies from uv.lock" - echo " โ€ข uv lock - update locks (patch + minor updates)" - echo " โ€ข uv lock --upgrade - update locks (with major versions)" - echo " โ€ข uv run pytest - run pytest without activating venv" -} - -setup_development_environment() { - check_system_requirements - setup_dependencies - configure_locale - setup_direnv - setup_pre_commit - setup_secrets - validate_project - print_completion -} - -setup_development_environment From 9e485807f0a0115b57270eb8f8e3be3ea982f2ac Mon Sep 17 00:00:00 2001 From: MLingerTechletes Date: Thu, 7 May 2026 15:03:23 +0200 Subject: [PATCH 140/199] Devcontainer adjustments (#31) * Added direnv, curl, 1password CLI install to devcontainer * added changes from pre-commit --- .devcontainer/Dockerfile | 10 ++++++++++ .devcontainer/devcontainer.json | 1 + .devcontainer/post-create.sh | 14 +++++++++++++- docs/1_usage.md | 3 +++ docs/7_devcontainers.md | 3 +++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 61284dcc..35fd1ca8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -8,11 +8,21 @@ ADD https://astral.sh/uv/${UV_VERSION}/install.sh /tmp/uv-installer.sh RUN env UV_UNMANAGED_INSTALL="/usr/local/bin" sh /tmp/uv-installer.sh \ && rm /tmp/uv-installer.sh +# Install direnv (Added by Michaรซl: 7-5-2026) RUN rm -f /etc/apt/sources.list.d/yarn.list \ && apt-get update \ && apt-get install -y --no-install-recommends \ bubblewrap \ socat \ + curl \ + direnv \ + && rm -rf /var/lib/apt/lists/* + +# Install 1Password CLI (Added by Michaรซl: 7-5-2026) +RUN curl -sS https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/$(dpkg --print-architecture) stable main" | tee /etc/apt/sources.list.d/1password.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends 1password-cli \ && rm -rf /var/lib/apt/lists/* ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 152675c3..b91c285c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,6 +7,7 @@ "PRE_COMMIT_HOME": "/home/vscode/.cache/pre-commit", "UV_CACHE_DIR": "/home/vscode/.cache/uv", "UV_LINK_MODE": "copy", + "UV_PROJECT_ENVIRONMENT": ".venv", "UV_PYTHON_DOWNLOADS": "never" }, "customizations": { diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 58629efc..a58f63db 100644 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -29,7 +29,19 @@ echo "Bootstrapping the container workspace with uv..." uv --version python --version -uv sync --frozen +# uv venv .venv +uv sync --frozen --all-groups + +# (Added by Michaรซl: 7-5-2026) +# load environment variables from .envrc +source .envrc + +# (Added by Michaรซl: 7-5-2026) +# ensure .envrc is sourced automatically when opening new terminals inside the container +if ! grep -q "source .envrc" ~/.bashrc 2>/dev/null; then + echo "# Auto-source .envrc for this workspace" >> ~/.bashrc + echo "if [[ -f \"${repo_root}/.envrc\" ]]; then source \"${repo_root}/.envrc\"; fi" >> ~/.bashrc +fi if [[ -d .git ]]; then uv run pre-commit install --install-hooks diff --git a/docs/1_usage.md b/docs/1_usage.md index b353f3f7..4a0d7104 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -90,6 +90,9 @@ uv lock --upgrade uv sync ``` +[Edited by: Michaรซl Linger 7-5-2026]: # +Alternatively, you can use uv `add ` (e.g. `uv add sqlalchemy`) which automatically updates pyproject.toml, resolves dependencies, and installs them into the current environment in one step. + Always use version constraints like `>=X.Y.Z,` (e.g. `uv add sqlalchemy`) which automatically updates pyproject.toml, resolves dependencies, and installs them into the current environment in one step. + The devcontainer does not replace the repository's dependency management model. It only provides the environment where that model runs. From e2ce5d2689e3293ddcb6eb7feccc453b053b8c2d Mon Sep 17 00:00:00 2001 From: Maarten Hofland Date: Mon, 18 May 2026 13:40:09 +0200 Subject: [PATCH 141/199] =?UTF-8?q?feat:=20add=20GitHub=20Actions=20workfl?= =?UTF-8?q?ow=20for=20automatic=20PR=20labeling=20based=20on=20=E2=80=A6?= =?UTF-8?q?=20(#32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add GitHub Actions workflow for automatic PR labeling based on branch names * chore: update coverage badge --------- Co-authored-by: MaartenHofland <197581742+MaartenHofland@users.noreply.github.com> --- .github/labeler.yml | 21 +++++++++++ .github/workflows/labeler.yml | 65 +++++++++++++++++++++++++++++++++++ coverage.svg | 2 +- pyproject.toml | 4 ++- 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..d1c3344f --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,21 @@ +# Automatic PR labeling based on file changes +# Used by actions/labeler@v6 in labeler.yml workflow +# Valid labels: breaking, security, feature, bug, refactor, question, docs + +# Documentation changes +docs: + - changed-files: + - any-glob-to-any-file: 'docs/**' + - any-glob-to-any-file: '*.md' + +# Security-related changes +security: + - changed-files: + - any-glob-to-any-file: 'utils/secrets.py' + - any-glob-to-any-file: '.github/dependabot.yml' + - any-glob-to-any-file: '.pre-commit-config.yaml' + +# Breaking changes (major dependency updates, API changes) +breaking: + - changed-files: + - any-glob-to-any-file: 'pyproject.toml' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 00000000..46ef623d --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,65 @@ +name: Labels +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + # For label-checker + - labeled + - unlabeled + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v6 + if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} + + # Add labels based on branch name pattern + - name: Add label based on branch + if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} + uses: actions/github-script@v7 + with: + script: | + const branchName = context.payload.pull_request.head.ref; + const labels = []; + + if (branchName.startsWith('feature/')) { + labels.push('feature'); + } else if (branchName.startsWith('bug/')) { + labels.push('bug'); + } else if (branchName.startsWith('refactor/')) { + labels.push('refactor'); + } else if (branchName.startsWith('security/')) { + labels.push('security'); + } else if (branchName.startsWith('breaking/')) { + labels.push('breaking'); + } + + if (labels.length > 0) { + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: labels + }); + console.log(`Added labels based on branch '${branchName}': ${labels.join(', ')}`); + } + + - run: echo "Done adding labels" + # Run this after labeler applied labels + check-labels: + needs: + - labeler + permissions: + pull-requests: read + runs-on: ubuntu-latest + steps: + - uses: docker://agilepathway/pull-request-label-checker:latest + with: + one_of: breaking,security,feature,bug,refactor,question,docs + repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/coverage.svg b/coverage.svg index ef52bd22..7175bd15 100644 --- a/coverage.svg +++ b/coverage.svg @@ -1 +1 @@ -coverage: 100.00%coverage100.00% \ No newline at end of file +coverage: 100.00%coverage100.00% \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index bebb6e8d..2afbf0b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,7 +195,9 @@ skips = ["B101", "B601"] # See docs/6_pre_commit_troubleshooting.md for explanation of each exclusion ignore = [ "CVE-2026-3219", # pip 26.0.1 pip handles concatenated tar and ZIP files as ZIP files regardless of filename or whether a file is both a tar and ZIP file. This behavior could result in confusing installation behavior, such as installing "incorrect" files according to the filename of the archive. New behavior only proceeds with installation if the file identifies uniquely as a ZIP or tar archive, not as both. - "CVE-2026-6357" + "CVE-2026-6357", + "CVE-2026-44431", # urllib3: Cross-origin redirect sensitive headers leak. Upgrade urllib3 to 2.7.0+ for fix. + "CVE-2026-44432", # urllib3: Decompression resource exhaustion in streaming API. Upgrade urllib3 to 2.7.0+ for fix. ] [tool.uv] From 8b77fab2e1ab44d7bfcca1eb6663793de6f3e7e8 Mon Sep 17 00:00:00 2001 From: Maarten Hofland Date: Mon, 18 May 2026 16:27:54 +0200 Subject: [PATCH 142/199] =?UTF-8?q?feat:=20add=20branch=20name=20validatio?= =?UTF-8?q?n=20script=20and=20update=20pre-commit=20configu=E2=80=A6=20(#3?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add branch name validation script and update pre-commit configuration * refactor: remove deprecated labeler configuration and update workflow steps * feat: enhance branch name validation script to include additional prefixes and improve handling for CI/CD environments --- .github/labeler.yml | 21 -------- .github/workflows/labeler.yml | 11 ++-- .pre-commit-config.yaml | 9 ++++ scripts/hooks/validate-branch-name.sh | 75 +++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 26 deletions(-) delete mode 100644 .github/labeler.yml create mode 100755 scripts/hooks/validate-branch-name.sh diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index d1c3344f..00000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Automatic PR labeling based on file changes -# Used by actions/labeler@v6 in labeler.yml workflow -# Valid labels: breaking, security, feature, bug, refactor, question, docs - -# Documentation changes -docs: - - changed-files: - - any-glob-to-any-file: 'docs/**' - - any-glob-to-any-file: '*.md' - -# Security-related changes -security: - - changed-files: - - any-glob-to-any-file: 'utils/secrets.py' - - any-glob-to-any-file: '.github/dependabot.yml' - - any-glob-to-any-file: '.pre-commit-config.yaml' - -# Breaking changes (major dependency updates, API changes) -breaking: - - changed-files: - - any-glob-to-any-file: 'pyproject.toml' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 46ef623d..9f613c78 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -16,16 +16,13 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v6 - if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} - - # Add labels based on branch name pattern + # Add labels based on branch name pattern (case-insensitive) - name: Add label based on branch if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} uses: actions/github-script@v7 with: script: | - const branchName = context.payload.pull_request.head.ref; + const branchName = context.payload.pull_request.head.ref.toLowerCase(); const labels = []; if (branchName.startsWith('feature/')) { @@ -38,6 +35,10 @@ jobs: labels.push('security'); } else if (branchName.startsWith('breaking/')) { labels.push('breaking'); + } else if (branchName.startsWith('question/')) { + labels.push('question'); + } else if (branchName.startsWith('docs/')) { + labels.push('docs'); } if (labels.length > 0) { diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6da0a7ac..f8ae7556 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,15 @@ repos: - repo: local hooks: + # Validate branch name follows naming convention + - id: validate-branch-name + name: Validate branch name format + entry: bash scripts/hooks/validate-branch-name.sh + language: system + always_run: true + pass_filenames: false + stages: [pre-commit] + # Strip notebook outputs - id: nbstripout-autoadd name: nbstripout (auto-add notebooks) diff --git a/scripts/hooks/validate-branch-name.sh b/scripts/hooks/validate-branch-name.sh new file mode 100755 index 00000000..ba0cecf7 --- /dev/null +++ b/scripts/hooks/validate-branch-name.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# Validate that branch names follow the required naming convention +# This ensures all branches start with: feature/, bug/, refactor/, security/, breaking/, question/, or docs/ +# Works both locally and in CI/CD environments (GitHub Actions) + +set -e + +# Get current branch name +# In local environment: use git rev-parse +# In GitHub Actions: use GITHUB_HEAD_REF environment variable +if [ -n "$GITHUB_HEAD_REF" ]; then + # Running in GitHub Actions + BRANCH_NAME="$GITHUB_HEAD_REF" +else + # Running locally + BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) +fi + +# Skip validation if we're in a detached HEAD state and not in GitHub Actions +# (shouldn't happen, but safety check) +if [ "$BRANCH_NAME" = "HEAD" ] && [ -z "$GITHUB_HEAD_REF" ]; then + echo "โญ๏ธ Skipping branch name validation (detached HEAD state)" + exit 0 +fi + +# Valid prefixes (case-insensitive) +VALID_PREFIXES=("feature/" "bug/" "refactor/" "security/" "breaking/" "question/" "docs/") + +# Convert branch name to lowercase for comparison +BRANCH_LOWER=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]') + +# Check if branch matches any valid prefix +VALID=false +for prefix in "${VALID_PREFIXES[@]}"; do + if [[ "$BRANCH_LOWER" == "$prefix"* ]]; then + VALID=true + break + fi +done + +# If invalid, show error and exit +if [ "$VALID" = false ]; then + cat << EOF >&2 + +โŒ BRANCH NAME VALIDATION FAILED + +Current branch: "$BRANCH_NAME" + +Branch names must start with one of: + โ€ข feature/ (new features) + โ€ข bug/ (bug fixes) + โ€ข refactor/ (code refactoring) + โ€ข security/ (security updates) + โ€ข breaking/ (breaking changes) + โ€ข question/ (questions/discussions) + โ€ข docs/ (documentation updates) + +Examples of valid branch names: + โ€ข feature/add-login-page + โ€ข bug/fix-memory-leak + โ€ข refactor/utils-module + โ€ข security/update-dependencies + โ€ข breaking/v2-api-redesign + โ€ข question/api-design-question + โ€ข docs/setup-guide + +To rename your branch, run: + git branch -m + +Then commit again. +EOF + exit 1 +fi + +exit 0 From ae748b75377a6d09a501cc0bd3d2903d281eed47 Mon Sep 17 00:00:00 2001 From: Maarten Hofland Date: Tue, 19 May 2026 08:38:50 +0200 Subject: [PATCH 143/199] fix: enhance branch name validation to allow protected branches for CI/CD (#34) --- scripts/hooks/validate-branch-name.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/hooks/validate-branch-name.sh b/scripts/hooks/validate-branch-name.sh index ba0cecf7..a32eaaa2 100755 --- a/scripts/hooks/validate-branch-name.sh +++ b/scripts/hooks/validate-branch-name.sh @@ -26,6 +26,11 @@ fi # Valid prefixes (case-insensitive) VALID_PREFIXES=("feature/" "bug/" "refactor/" "security/" "breaking/" "question/" "docs/") +# Allow protected branches (main, staging, etc.) specifically for CI/CD or direct maintenance +if [[ "$BRANCH_NAME" == "main" || "$BRANCH_NAME" == "staging" || "$BRANCH_NAME" == "master" ]]; then + exit 0 +fi + # Convert branch name to lowercase for comparison BRANCH_LOWER=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]') From 265217d8379699dfff56559a033943b13aeb02e2 Mon Sep 17 00:00:00 2001 From: Maarten Hofland Date: Tue, 19 May 2026 11:55:36 +0200 Subject: [PATCH 144/199] bugfix: multiple label allowance (#35) * bugfix: multiple layer allowance * chore: update coverage badge --------- Co-authored-by: MaartenHofland <197581742+MaartenHofland@users.noreply.github.com> --- .github/workflows/labeler.yml | 3 ++- coverage.svg | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 9f613c78..318a600a 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -62,5 +62,6 @@ jobs: steps: - uses: docker://agilepathway/pull-request-label-checker:latest with: - one_of: breaking,security,feature,bug,refactor,question,docs + any_of: breaking,security,feature,bug,refactor,question,docs + minimum: 1 repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/coverage.svg b/coverage.svg index 7175bd15..ef52bd22 100644 --- a/coverage.svg +++ b/coverage.svg @@ -1 +1 @@ -coverage: 100.00%coverage100.00% \ No newline at end of file +coverage: 100.00%coverage100.00% \ No newline at end of file From 5812d179a0ac88a1f1b6d14b0f3b2e6a7acd892a Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 1 Jun 2026 11:12:26 +0000 Subject: [PATCH 145/199] Fix devcontainer setup --- .agents/skills/caveman/README.md | 48 + .agents/skills/caveman/SKILL.md | 74 ++ .agents/skills/graphify/.graphify_version | 1 + .agents/skills/graphify/SKILL.md | 1317 +++++++++++++++++++++ .codex/config.toml | 2 + .codex/hooks.json | 28 + .devcontainer/Dockerfile | 16 +- .devcontainer/devcontainer.json | 10 +- .devcontainer/post-create.sh | 20 +- .vscode/extensions.json | 4 +- uv.lock | 6 +- 11 files changed, 1493 insertions(+), 33 deletions(-) create mode 100644 .agents/skills/caveman/README.md create mode 100644 .agents/skills/caveman/SKILL.md create mode 100644 .agents/skills/graphify/.graphify_version create mode 100644 .agents/skills/graphify/SKILL.md create mode 100644 .codex/config.toml create mode 100644 .codex/hooks.json diff --git a/.agents/skills/caveman/README.md b/.agents/skills/caveman/README.md new file mode 100644 index 00000000..d749b834 --- /dev/null +++ b/.agents/skills/caveman/README.md @@ -0,0 +1,48 @@ +# caveman + +Talk like smart caveman. Same brain, fewer tokens. + +## What it does + +Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped. + +Six intensity levels: + +| Level | What change | +|-------|-------------| +| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. | +| `full` | Default. Drop articles, fragments OK, short synonyms. | +| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. | +| `wenyan-lite` | Classical Chinese register, light compression. | +| `wenyan-full` | Maximum ๆ–‡่จ€ๆ–‡. 80-90% character reduction. | +| `wenyan-ultra` | Extreme classical compression. | + +Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part. + +## How to invoke + +``` +/caveman # full mode (default) +/caveman lite # lighter compression +/caveman ultra # extreme compression +/caveman wenyan # classical Chinese +stop caveman # back to normal prose +``` + +## Example output + +Question: "Why does my React component re-render?" + +Normal prose: +> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue. + +Caveman (full): +> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`. + +Caveman (ultra): +> Inline obj prop โ†’ new ref โ†’ re-render. `useMemo`. + +## See also + +- [`SKILL.md`](./SKILL.md) โ€” full LLM-facing instructions +- [Caveman README](../../README.md) โ€” repo overview, install, benchmarks diff --git a/.agents/skills/caveman/SKILL.md b/.agents/skills/caveman/SKILL.md new file mode 100644 index 00000000..09ea3ef2 --- /dev/null +++ b/.agents/skills/caveman/SKILL.md @@ -0,0 +1,74 @@ +--- +name: caveman +description: > + Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman + while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, + wenyan-lite, wenyan-full, wenyan-ultra. + Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", + "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested. +--- + +Respond terse like smart caveman. All technical substance stay. Only fluff die. + +## Persistence + +ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode". + +Default: **full**. Switch: `/caveman lite|full|ultra`. + +## Rules + +Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact. + +Pattern: `[thing] [action] [reason]. [next step].` + +Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." +Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" + +## Intensity + +| Level | What change | +|-------|------------| +| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight | +| **full** | Drop articles, fragments OK, short synonyms. Classic caveman | +| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X โ†’ Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate | +| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register | +| **wenyan-full** | Maximum classical terseness. Fully ๆ–‡่จ€ๆ–‡. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (ไน‹/ไนƒ/็‚บ/ๅ…ถ) | +| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse | + +Example โ€” "Why React component re-render?" +- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`." +- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`." +- ultra: "Inline obj prop โ†’ new ref โ†’ re-render. `useMemo`." +- wenyan-lite: "็ต„ไปถ้ ป้‡็นช๏ผŒไปฅๆฏ็นชๆ–ฐ็”Ÿๅฐ่ฑกๅƒ็…งๆ•…ใ€‚ไปฅ useMemo ๅŒ…ไน‹ใ€‚" +- wenyan-full: "็‰ฉๅ‡บๆ–ฐๅƒ็…ง๏ผŒ่‡ด้‡็นชใ€‚useMemo .Wrapไน‹ใ€‚" +- wenyan-ultra: "ๆ–ฐๅƒ็…งโ†’้‡็นชใ€‚useMemo Wrapใ€‚" + +Example โ€” "Explain database connection pooling." +- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead." +- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead." +- ultra: "Pool = reuse DB conn. Skip handshake โ†’ fast under load." +- wenyan-full: "ๆฑ reuse open connectionใ€‚ไธๆฏreqๆ–ฐ้–‹ใ€‚skip handshake overheadใ€‚" +- wenyan-ultra: "ๆฑ reuse connใ€‚skip handshake โ†’ fastใ€‚" + +## Auto-Clarity + +Drop caveman when: +- Security warnings +- Irreversible action confirmations +- Multi-step sequences where fragment order or omitted conjunctions risk misread +- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` โ€” order unclear without articles/conjunctions) +- User asks to clarify or repeats question + +Resume caveman after clear part done. + +Example โ€” destructive op: +> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. +> ```sql +> DROP TABLE users; +> ``` +> Caveman resume. Verify backup exist first. + +## Boundaries + +Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end. diff --git a/.agents/skills/graphify/.graphify_version b/.agents/skills/graphify/.graphify_version new file mode 100644 index 00000000..85ccf6fa --- /dev/null +++ b/.agents/skills/graphify/.graphify_version @@ -0,0 +1 @@ +0.8.26 diff --git a/.agents/skills/graphify/SKILL.md b/.agents/skills/graphify/SKILL.md new file mode 100644 index 00000000..7a2bc66b --- /dev/null +++ b/.agents/skills/graphify/SKILL.md @@ -0,0 +1,1317 @@ +--- +name: graphify +description: "any input (code, docs, papers, images) โ†’ knowledge graph โ†’ clustered communities โ†’ HTML + JSON + audit report. Use when user asks any question about a codebase, project content, architecture, or file relationships โ€” especially if graphify-out/ exists. Provides persistent graph with god nodes, community detection, and BFS/DFS query tools." +trigger: /graphify +--- + +# /graphify + +Turn any folder of files into a navigable knowledge graph with community detection, an honest audit trail, and three outputs: interactive HTML, GraphRAG-ready JSON, and a plain-language GRAPH_REPORT.md. + +## Usage + +``` +/graphify # full pipeline on current directory โ†’ Obsidian vault +/graphify # full pipeline on specific path +/graphify --mode deep # thorough extraction, richer INFERRED edges +/graphify --update # incremental - re-extract only new/changed files +/graphify --cluster-only # rerun clustering on existing graph +/graphify --no-viz # skip visualization, just report + JSON +/graphify --html # (HTML is generated by default - this flag is a no-op) +/graphify --svg # also export graph.svg (embeds in Notion, GitHub) +/graphify --graphml # export graph.graphml (Gephi, yEd) +/graphify --neo4j # generate graphify-out/cypher.txt for Neo4j +/graphify --neo4j-push bolt://localhost:7687 # push directly to Neo4j +/graphify --mcp # start MCP stdio server for agent access +/graphify --watch # watch folder, auto-rebuild on code changes (no LLM needed) +/graphify add # fetch URL, save to ./raw, update graph +/graphify add --author "Name" # tag who wrote it +/graphify add --contributor "Name" # tag who added it to the corpus +/graphify query "" # BFS traversal - broad context +/graphify query "" --dfs # DFS - trace a specific path +/graphify query "" --budget 1500 # cap answer at N tokens +/graphify path "AuthModule" "Database" # shortest path between two concepts +/graphify explain "SwinTransformer" # plain-language explanation of a node +``` + +## What graphify is for + +graphify is built around Andrej Karpathy's /raw folder workflow: drop anything into a folder - papers, tweets, screenshots, code, notes - and get a structured knowledge graph that shows you what you didn't know was connected. + +Three things it does that your AI assistant alone cannot: +1. **Persistent graph** - relationships are stored in `graphify-out/graph.json` and survive across sessions. Ask questions weeks later without re-reading everything. +2. **Honest audit trail** - every edge is tagged EXTRACTED, INFERRED, or AMBIGUOUS. You know what was found vs invented. +3. **Cross-document surprise** - community detection finds connections between concepts in different files that you would never think to ask about directly. + +Use it for: +- A codebase you're new to (understand architecture before touching anything) +- A reading list (papers + tweets + notes โ†’ one navigable graph) +- A research corpus (citation graph + concept graph in one) +- Your personal /raw folder (drop everything in, let it grow, query it) + +## What You Must Do When Invoked + +If the user invoked `/graphify --help` or `/graphify -h` (with no other arguments), print the contents of the `## Usage` section above verbatim and stop. Do not run any commands, do not detect files, do not default the path to `.`. Just print the Usage block and return. + +If no path was given, use `.` (current directory). Do not ask the user for a path. + +If `graphify-out/` exists, use `graphify query`, `graphify explain`, or `graphify path` for orientation before broad grep, rg, or multi-file reads. Dirty `graphify-out/` artifacts are expected after hooks or incremental updates; dirty graph files are not a reason to skip Graphify. Only skip Graphify if the task is specifically about stale or incorrect graph output, or the user explicitly says not to use it. + +Follow these steps in order. Do not skip steps. + +### Step 1 - Ensure graphify is installed + +```bash +# Detect the correct Python interpreter (handles uv tool, pipx, venv, system installs) +PYTHON="" +GRAPHIFY_BIN=$(which graphify 2>/dev/null) +# 1. uv tool installs โ€” most reliable on modern Mac/Linux +if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then + _UV_PY=$(uv tool run graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null) + if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi +fi +# 2. Read shebang from graphify binary (pipx and direct pip installs) +if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then + _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + case "$_SHEBANG" in + *[!a-zA-Z0-9/_.-]*) ;; + *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; + esac +fi +# 3. Fall back to python3 +if [ -z "$PYTHON" ]; then PYTHON="python3"; fi +if ! "$PYTHON" -c "import graphify" 2>/dev/null; then + if command -v uv >/dev/null 2>&1; then + uv tool install --upgrade graphifyy -q 2>&1 | tail -3 + _UV_PY=$(uv tool run graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null) + if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi + else + "$PYTHON" -m pip install graphifyy -q 2>/dev/null \ + || "$PYTHON" -m pip install graphifyy -q --break-system-packages 2>&1 | tail -3 + fi +fi +# Write interpreter path for all subsequent steps (persists across invocations) +mkdir -p graphify-out +"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)" +``` + +If the import succeeds, print nothing and move straight to Step 2. + +**In every subsequent bash block, replace `python3` with `$(cat graphify-out/.graphify_python)` to use the correct interpreter.** + +### Step 2 - Detect files + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from graphify.detect import detect +from pathlib import Path +result = detect(Path('INPUT_PATH')) +print(json.dumps(result)) +" > .graphify_detect.json +``` + +Replace INPUT_PATH with the actual path the user provided. Do NOT cat or print the JSON - read it silently and present a clean summary instead: + +``` +Corpus: X files ยท ~Y words + code: N files (.py .ts .go ...) + docs: N files (.md .txt ...) + papers: N files (.pdf ...) + images: N files + video: N files (.mp4 .mp3 ...) +``` + +Omit any category with 0 files from the summary. + +Then act on it: +- If `total_files` is 0: stop with "No supported files found in [path]." +- If `skipped_sensitive` is non-empty: mention file count skipped, not the file names. +- If `total_words` > 2,000,000 OR `total_files` > 200: show the warning and the top 5 subdirectories by file count, then ask which subfolder to run on. Wait for the user's answer before proceeding. +- Otherwise: proceed directly to Step 2.5 if video files were detected, or Step 3 if not. + +### Step 2.5 - Transcribe video / audio files (only if video files detected) + +Skip this step entirely if `detect` returned zero `video` files. + +Video and audio files cannot be read directly. Transcribe them to text first, then treat the transcripts as doc files in Step 3. + +**Strategy:** Read the god nodes from the detect output or analysis file. You are already a language model โ€” write a one-sentence domain hint yourself from those labels. Then pass it to Whisper as the initial prompt. No separate API call needed. + +**However**, if the corpus has *only* video files and no other docs/code, use the generic fallback prompt: `"Use proper punctuation and paragraph breaks."` + +**Step 1 - Write the Whisper prompt yourself.** + +Read the top god node labels from detect output or analysis, then compose a short domain hint sentence, for example: + +- Labels: `transformer, attention, encoder, decoder` โ†’ `"Machine learning research on transformer architectures and attention mechanisms. Use proper punctuation and paragraph breaks."` +- Labels: `kubernetes, deployment, pod, helm` โ†’ `"DevOps discussion about Kubernetes deployments and Helm charts. Use proper punctuation and paragraph breaks."` + +Set it as `GRAPHIFY_WHISPER_PROMPT` in the environment before running the transcription command. + +**Step 2 - Transcribe:** + +```bash +$(cat graphify-out/.graphify_python) -c " +import json, os +from pathlib import Path +from graphify.transcribe import transcribe_all + +detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text()) +video_files = detect.get('files', {}).get('video', []) +prompt = os.environ.get('GRAPHIFY_WHISPER_PROMPT', 'Use proper punctuation and paragraph breaks.') + +transcript_paths = transcribe_all(video_files, initial_prompt=prompt) +print(json.dumps(transcript_paths)) +" > graphify-out/.graphify_transcripts.json +``` + +After transcription: +- Read the transcript paths from `graphify-out/.graphify_transcripts.json` +- Add them to the docs list before dispatching semantic subagents in Step 3B +- Print how many transcripts were created: `Transcribed N video file(s) -> treating as docs` +- If transcription fails for a file, print a warning and continue with the rest + +**Whisper model:** Default is `base`. If the user passed `--whisper-model `, set `GRAPHIFY_WHISPER_MODEL=` in the environment before running the command above. + +### Step 3 - Extract entities and relationships + +**Before starting:** note whether `--mode deep` was given. You must pass `DEEP_MODE=true` to every subagent in Step B2 if it was. Track this from the original invocation - do not lose it. + +This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (your AI model, costs tokens). + +**Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** + +Note: Parallelizing AST + semantic saves 5-15s on large corpora. AST is deterministic and fast; start it while subagents are processing docs/papers. + +#### Part A - Structural extraction for code files + +For any code files detected, run AST extraction in parallel with Part B subagents: + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.extract import collect_files, extract +from pathlib import Path +import json + +code_files = [] +detect = json.loads(Path('.graphify_detect.json').read_text()) +for f in detect.get('files', {}).get('code', []): + code_files.extend(collect_files(Path(f)) if Path(f).is_dir() else [Path(f)]) + +if code_files: + result = extract(code_files) + Path('.graphify_ast.json').write_text(json.dumps(result, indent=2)) + print(f'AST: {len(result[\"nodes\"])} nodes, {len(result[\"edges\"])} edges') +else: + Path('.graphify_ast.json').write_text(json.dumps({'nodes':[],'edges':[],'input_tokens':0,'output_tokens':0})) + print('No code files - skipping AST extraction') +" +``` + +#### Part B - Semantic extraction (parallel subagents) + +**Fast path:** If detection found zero docs, papers, and images (code-only corpus), skip Part B entirely and go straight to Part C. AST handles code - there is nothing for semantic subagents to do. + +**MANDATORY: You MUST use the Agent tool here. Reading files yourself one-by-one is forbidden - it is 5-10x slower. If you do not use the Agent tool you are doing this wrong.** + +Before dispatching subagents, print a timing estimate: +- Load `total_words` and file counts from `.graphify_detect.json` +- Estimate agents needed: `ceil(uncached_non_code_files / 22)` (chunk size is 20-25) +- Estimate time: ~45s per agent batch (they run in parallel, so total โ‰ˆ 45s ร— ceil(agents/parallel_limit)) +- Print: "Semantic extraction: ~N files โ†’ X agents, estimated ~Ys" + +**Step B0 - Check extraction cache first** + +Before dispatching any subagents, check which files already have cached extraction results: + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from graphify.cache import check_semantic_cache +from pathlib import Path + +detect = json.loads(Path('.graphify_detect.json').read_text()) +all_files = [f for files in detect['files'].values() for f in files] + +cached_nodes, cached_edges, cached_hyperedges, uncached = check_semantic_cache(all_files) + +if cached_nodes or cached_edges or cached_hyperedges: + Path('.graphify_cached.json').write_text(json.dumps({'nodes': cached_nodes, 'edges': cached_edges, 'hyperedges': cached_hyperedges})) +Path('.graphify_uncached.txt').write_text('\n'.join(uncached)) +print(f'Cache: {len(all_files)-len(uncached)} files hit, {len(uncached)} files need extraction') +" +``` + +Only dispatch subagents for files listed in `.graphify_uncached.txt`. If all files are cached, skip to Part C directly. + +**Step B1 - Split into chunks** + +Load files from `.graphify_uncached.txt`. Split into chunks of 20-25 files each. Each image gets its own chunk (vision needs separate context). When splitting, group files from the same directory together so related artifacts land in the same chunk and cross-file relationships are more likely to be extracted. + +**Step B2 - Dispatch ALL subagents in a single message (Codex)** + +> **Codex platform:** Uses `spawn_agent` + `wait_agent` + `close_agent` instead of the Agent tool. +> Requires `multi_agent = true` under `[features]` in `~/.codex/config.toml`. +> If `spawn_agent` is unavailable, tell the user to add that config and restart Codex. + +Call `spawn_agent` once per chunk โ€” ALL in the same response so they run in parallel. Build the message by wrapping the extraction prompt below in task-delegation framing: + +``` +spawn_agent(agent_type="worker", message="Your task is to perform the following. Follow the instructions below exactly.\n\n\n[extraction prompt below, with FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE substituted]\n\n\nExecute this now. Output ONLY the structured JSON response.") +``` + +After all agents are dispatched, collect results sequentially: +``` +result = wait_agent(handle); close_agent(handle) # repeat per handle +``` + +Parse each result as JSON. Accumulate nodes/edges/hyperedges across all results and write to `.graphify_semantic_new.json`. + +The extraction prompt each subagent receives (substitute FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE): + +``` +You are a graphify extraction subagent. Read the files listed and extract a knowledge graph fragment. +Output ONLY valid JSON matching the schema below - no explanation, no markdown fences, no preamble. + +Files (chunk CHUNK_NUM of TOTAL_CHUNKS): +FILE_LIST + +Rules: +- EXTRACTED: relationship explicit in source (import, call, citation, "see ยง3.2") +- INFERRED: reasonable inference (shared data structure, implied dependency) +- AMBIGUOUS: uncertain - flag for review, do not omit + +Code files: focus on semantic edges AST cannot find (call relationships, shared data, arch patterns). + Do not re-extract imports - AST already has those. +Doc/paper files: extract named concepts, entities, citations. For rationale (WHY decisions were made, trade-offs, design intent): store as a `rationale` attribute on the relevant named node โ€” do NOT create a separate rationale node or fragment node. Only create a node for something that is itself a named entity or concept. Use the closest existing `file_type` (`document` for prose, `code` for code-derived concepts). Do NOT invent file_types like `concept` or `rationale` โ€” valid values are only `code|document|paper|image`. +Code files: when adding `calls` edges, source MUST be the caller (the function/class doing the calling), target MUST be the callee. Never reverse this direction. +Image files: use vision to understand what the image IS - do not just OCR. + UI screenshot: layout patterns, design decisions, key elements, purpose. + Chart: metric, trend/insight, data source. + Tweet/post: claim as node, author, concepts mentioned. + Diagram: components and connections. + Research figure: what it demonstrates, method, result. + Handwritten/whiteboard: ideas and arrows, mark uncertain readings AMBIGUOUS. + +DEEP_MODE (if --mode deep was given): be aggressive with INFERRED edges - indirect deps, + shared assumptions, latent couplings. Mark uncertain ones AMBIGUOUS instead of omitting. + +Semantic similarity: if two concepts in this chunk solve the same problem or represent the same idea without any structural link (no import, no call, no citation), add a `semantically_similar_to` edge marked INFERRED with a confidence_score reflecting how similar they are (0.6-0.95). Examples: +- Two functions that both validate user input but never call each other +- A class in code and a concept in a paper that describe the same algorithm +- Two error types that handle the same failure mode differently +Only add these when the similarity is genuinely non-obvious and cross-cutting. Do not add them for trivially similar things. + +Hyperedges: if 3 or more nodes clearly participate together in a shared concept, flow, or pattern that is not captured by pairwise edges alone, add a hyperedge to a top-level `hyperedges` array. Examples: +- All classes that implement a common protocol or interface +- All functions in an authentication flow (even if they don't all call each other) +- All concepts from a paper section that form one coherent idea +Use sparingly โ€” only when the group relationship adds information beyond the pairwise edges. Maximum 3 hyperedges per chunk. + +If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author, + contributor onto every node from that file. + +confidence_score is REQUIRED on every edge - never omit it, never use 0.5 as a default: +- EXTRACTED edges: confidence_score = 1.0 always +- INFERRED edges: reason about each edge individually. + Direct structural evidence (shared data structure, clear dependency): 0.8-0.9. + Reasonable inference with some uncertainty: 0.6-0.7. + Weak or speculative: 0.4-0.5. Most edges should be 0.6-0.9, not 0.5. +- AMBIGUOUS edges: 0.1-0.3 + +Output exactly this JSON (no other text): +{"nodes":[{"id":"filestem_entityname","label":"Human Readable Name","file_type":"code|document|paper|image","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to|rationale_for","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"hyperedges":[{"id":"snake_case_id","label":"Human Readable Label","nodes":["node_id1","node_id2","node_id3"],"relation":"participate_in|implement|form","confidence":"EXTRACTED|INFERRED","confidence_score":0.75,"source_file":"relative/path"}],"input_tokens":0,"output_tokens":0} +``` + +**Step B3 - Collect, cache, and merge** + +Wait for all subagents. For each result: +- Check that `graphify-out/.graphify_chunk_NN.json` exists on disk โ€” this is the success signal +- If the file exists and contains valid JSON with `nodes` and `edges`, include it and save to cache +- If the file is missing, the subagent was likely dispatched as read-only (Explore type) โ€” print a warning: "chunk N missing from disk โ€” subagent may have been read-only. Re-run with general-purpose agent." Do not silently skip. +- If a subagent failed or returned invalid JSON, print a warning and skip that chunk - do not abort + +If more than half the chunks failed or are missing, stop and tell the user to re-run and ensure `subagent_type="general-purpose"` is used. + +Merge all chunk files into `.graphify_semantic_new.json`. **After each Agent call completes, read the real token counts from the Agent tool result's `usage` field and write them back into the chunk JSON before merging** โ€” the chunk JSON itself always has placeholder zeros. Then run: +```bash +$(cat graphify-out/.graphify_python) -c " +import json, glob +from pathlib import Path +from graphify.semantic_cleanup import load_validated_semantic_fragment, sanitize_semantic_fragment + +chunks = sorted(glob.glob('graphify-out/.graphify_chunk_*.json')) +all_nodes, all_edges, all_hyperedges = [], [], [] +total_in, total_out = 0, 0 +for c in chunks: + d, errors = load_validated_semantic_fragment(Path(c)) + if errors: + print(f'Skipping invalid chunk {c}: ' + '; '.join(errors[:3])) + continue + d = sanitize_semantic_fragment(d) + all_nodes += d.get('nodes', []) + all_edges += d.get('edges', []) + all_hyperedges += d.get('hyperedges', []) + total_in += d.get('input_tokens', 0) + total_out += d.get('output_tokens', 0) +Path('graphify-out/.graphify_semantic_new.json').write_text(json.dumps({ + 'nodes': all_nodes, 'edges': all_edges, 'hyperedges': all_hyperedges, + 'input_tokens': total_in, 'output_tokens': total_out, +}, indent=2)) +print(f'Merged {len(chunks)} chunks: {total_in:,} in / {total_out:,} out tokens') +" +``` + +Save new results to cache: +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from graphify.cache import save_semantic_cache +from pathlib import Path + +new = json.loads(Path('.graphify_semantic_new.json').read_text()) if Path('.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]} +saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', [])) +print(f'Cached {saved} files') +" +``` + +Merge cached + new results into `.graphify_semantic.json`: +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +from graphify.semantic_cleanup import sanitize_semantic_fragment + +cached = json.loads(Path('.graphify_cached.json').read_text()) if Path('.graphify_cached.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]} +new = json.loads(Path('.graphify_semantic_new.json').read_text()) if Path('.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]} + +all_nodes = cached['nodes'] + new.get('nodes', []) +all_edges = cached['edges'] + new.get('edges', []) +all_hyperedges = cached.get('hyperedges', []) + new.get('hyperedges', []) +seen = set() +deduped = [] +for n in all_nodes: + if n['id'] not in seen: + seen.add(n['id']) + deduped.append(n) + +merged = { + 'nodes': deduped, + 'edges': all_edges, + 'hyperedges': all_hyperedges, + 'input_tokens': new.get('input_tokens', 0), + 'output_tokens': new.get('output_tokens', 0), +} +merged = sanitize_semantic_fragment(merged) +Path('.graphify_semantic.json').write_text(json.dumps(merged, indent=2)) +print(f'Extraction complete - {len(deduped)} nodes, {len(all_edges)} edges ({len(cached[\"nodes\"])} from cache, {len(new.get(\"nodes\",[]))} new)') +" +``` +Clean up temp files: `rm -f .graphify_cached.json .graphify_uncached.txt .graphify_semantic_new.json` + +#### Part C - Merge AST + semantic into final extraction + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from pathlib import Path +from graphify.semantic_cleanup import sanitize_semantic_fragment + +ast = json.loads(Path('.graphify_ast.json').read_text()) +sem = json.loads(Path('.graphify_semantic.json').read_text()) + +# Merge: AST nodes first, semantic nodes deduplicated by id +seen = {n['id'] for n in ast['nodes']} +merged_nodes = list(ast['nodes']) +for n in sem['nodes']: + if n['id'] not in seen: + merged_nodes.append(n) + seen.add(n['id']) + +merged_edges = ast['edges'] + sem['edges'] +merged_hyperedges = sem.get('hyperedges', []) +merged = { + 'nodes': merged_nodes, + 'edges': merged_edges, + 'hyperedges': merged_hyperedges, + 'input_tokens': sem.get('input_tokens', 0), + 'output_tokens': sem.get('output_tokens', 0), +} +merged = sanitize_semantic_fragment(merged) +Path('.graphify_extract.json').write_text(json.dumps(merged, indent=2)) +total = len(merged_nodes) +edges = len(merged_edges) +print(f'Merged: {total} nodes, {edges} edges ({len(ast[\"nodes\"])} AST + {len(sem[\"nodes\"])} semantic)') +" +``` + +### Step 4 - Build graph, cluster, analyze, generate outputs + +```bash +mkdir -p graphify-out +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.build import build_from_json +from graphify.cluster import cluster, score_all +from graphify.analyze import god_nodes, surprising_connections, suggest_questions +from graphify.report import generate +from graphify.export import to_json +from pathlib import Path + +extraction = json.loads(Path('.graphify_extract.json').read_text()) +detection = json.loads(Path('.graphify_detect.json').read_text()) + +G = build_from_json(extraction) +communities = cluster(G) +cohesion = score_all(G, communities) +tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)} +gods = god_nodes(G) +surprises = surprising_connections(G, communities) +labels = {cid: 'Community ' + str(cid) for cid in communities} +# Placeholder questions - regenerated with real labels in Step 5 +questions = suggest_questions(G, communities, labels) + +report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) +Path('graphify-out/GRAPH_REPORT.md').write_text(report) +to_json(G, communities, 'graphify-out/graph.json') + +analysis = { + 'communities': {str(k): v for k, v in communities.items()}, + 'cohesion': {str(k): v for k, v in cohesion.items()}, + 'gods': gods, + 'surprises': surprises, + 'questions': questions, +} +Path('.graphify_analysis.json').write_text(json.dumps(analysis, indent=2)) +if G.number_of_nodes() == 0: + print('ERROR: Graph is empty - extraction produced no nodes.') + print('Possible causes: all files were skipped, binary-only corpus, or extraction failed.') + raise SystemExit(1) +print(f'Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges, {len(communities)} communities') +" +``` + +If this step prints `ERROR: Graph is empty`, stop and tell the user what happened - do not proceed to labeling or visualization. + +Replace INPUT_PATH with the actual path. + +### Step 5 - Label communities + +Read `.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). + +Then regenerate the report and save the labels for the visualizer: + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.build import build_from_json +from graphify.cluster import score_all +from graphify.analyze import god_nodes, surprising_connections, suggest_questions +from graphify.report import generate +from pathlib import Path + +extraction = json.loads(Path('.graphify_extract.json').read_text()) +detection = json.loads(Path('.graphify_detect.json').read_text()) +analysis = json.loads(Path('.graphify_analysis.json').read_text()) + +G = build_from_json(extraction) +communities = {int(k): v for k, v in analysis['communities'].items()} +cohesion = {int(k): v for k, v in analysis['cohesion'].items()} +tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)} + +# LABELS - replace these with the names you chose above +labels = LABELS_DICT + +# Regenerate questions with real community labels (labels affect question phrasing) +questions = suggest_questions(G, communities, labels) + +report = generate(G, communities, cohesion, labels, analysis['gods'], analysis['surprises'], detection, tokens, 'INPUT_PATH', suggested_questions=questions) +Path('graphify-out/GRAPH_REPORT.md').write_text(report) +Path('.graphify_labels.json').write_text(json.dumps({str(k): v for k, v in labels.items()})) +print('Report updated with community labels') +" +``` + +Replace `LABELS_DICT` with the actual dict you constructed (e.g. `{0: "Attention Mechanism", 1: "Training Pipeline"}`). +Replace INPUT_PATH with the actual path. + +### Step 6 - Generate Obsidian vault (opt-in) + HTML + +**Generate HTML always** (unless `--no-viz`). **Obsidian vault only if `--obsidian` was explicitly given** โ€” skip it otherwise, it generates one file per node. + +If `--obsidian` was given: + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.build import build_from_json +from graphify.export import to_obsidian, to_canvas +from pathlib import Path + +extraction = json.loads(Path('.graphify_extract.json').read_text()) +analysis = json.loads(Path('.graphify_analysis.json').read_text()) +labels_raw = json.loads(Path('.graphify_labels.json').read_text()) if Path('.graphify_labels.json').exists() else {} + +G = build_from_json(extraction) +communities = {int(k): v for k, v in analysis['communities'].items()} +cohesion = {int(k): v for k, v in analysis['cohesion'].items()} +labels = {int(k): v for k, v in labels_raw.items()} + +n = to_obsidian(G, communities, 'graphify-out/obsidian', community_labels=labels or None, cohesion=cohesion) +print(f'Obsidian vault: {n} notes in graphify-out/obsidian/') + +to_canvas(G, communities, 'graphify-out/obsidian/graph.canvas', community_labels=labels or None) +print('Canvas: graphify-out/obsidian/graph.canvas - open in Obsidian for structured community layout') +print() +print('Open graphify-out/obsidian/ as a vault in Obsidian.') +print(' Graph view - nodes colored by community (set automatically)') +print(' graph.canvas - structured layout with communities as groups') +print(' _COMMUNITY_* - overview notes with cohesion scores and dataview queries') +" +``` + +Generate the HTML graph (always, unless `--no-viz`): + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.build import build_from_json +from graphify.export import to_html +from pathlib import Path + +extraction = json.loads(Path('.graphify_extract.json').read_text()) +analysis = json.loads(Path('.graphify_analysis.json').read_text()) +labels_raw = json.loads(Path('.graphify_labels.json').read_text()) if Path('.graphify_labels.json').exists() else {} + +G = build_from_json(extraction) +communities = {int(k): v for k, v in analysis['communities'].items()} +labels = {int(k): v for k, v in labels_raw.items()} + +if G.number_of_nodes() > 5000: + print(f'Graph has {G.number_of_nodes()} nodes - too large for HTML viz. Use Obsidian vault instead.') +else: + to_html(G, communities, 'graphify-out/graph.html', community_labels=labels or None) + print('graph.html written - open in any browser, no server needed') +" +``` + +### Step 7 - Neo4j export (only if --neo4j or --neo4j-push flag) + +**If `--neo4j`** - generate a Cypher file for manual import: + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.build import build_from_json +from graphify.export import to_cypher +from pathlib import Path + +G = build_from_json(json.loads(Path('.graphify_extract.json').read_text())) +to_cypher(G, 'graphify-out/cypher.txt') +print('cypher.txt written - import with: cypher-shell < graphify-out/cypher.txt') +" +``` + +**If `--neo4j-push `** - push directly to a running Neo4j instance. Ask the user for credentials if not provided: + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.build import build_from_json +from graphify.cluster import cluster +from graphify.export import push_to_neo4j +from pathlib import Path + +extraction = json.loads(Path('.graphify_extract.json').read_text()) +analysis = json.loads(Path('.graphify_analysis.json').read_text()) +G = build_from_json(extraction) +communities = {int(k): v for k, v in analysis['communities'].items()} + +result = push_to_neo4j(G, uri='NEO4J_URI', user='NEO4J_USER', password='NEO4J_PASSWORD', communities=communities) +print(f'Pushed to Neo4j: {result[\"nodes\"]} nodes, {result[\"edges\"]} edges') +" +``` + +Replace `NEO4J_URI`, `NEO4J_USER`, `NEO4J_PASSWORD` with actual values. Default URI is `bolt://localhost:7687`, default user is `neo4j`. Uses MERGE - safe to re-run without creating duplicates. + +### Step 7b - SVG export (only if --svg flag) + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.build import build_from_json +from graphify.export import to_svg +from pathlib import Path + +extraction = json.loads(Path('.graphify_extract.json').read_text()) +analysis = json.loads(Path('.graphify_analysis.json').read_text()) +labels_raw = json.loads(Path('.graphify_labels.json').read_text()) if Path('.graphify_labels.json').exists() else {} + +G = build_from_json(extraction) +communities = {int(k): v for k, v in analysis['communities'].items()} +labels = {int(k): v for k, v in labels_raw.items()} + +to_svg(G, communities, 'graphify-out/graph.svg', community_labels=labels or None) +print('graph.svg written - embeds in Obsidian, Notion, GitHub READMEs') +" +``` + +### Step 7c - GraphML export (only if --graphml flag) + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from graphify.build import build_from_json +from graphify.export import to_graphml +from pathlib import Path + +extraction = json.loads(Path('.graphify_extract.json').read_text()) +analysis = json.loads(Path('.graphify_analysis.json').read_text()) + +G = build_from_json(extraction) +communities = {int(k): v for k, v in analysis['communities'].items()} + +to_graphml(G, communities, 'graphify-out/graph.graphml') +print('graph.graphml written - open in Gephi, yEd, or any GraphML tool') +" +``` + +### Step 7d - MCP server (only if --mcp flag) + +```bash +python3 -m graphify.serve graphify-out/graph.json +``` + +This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. + +To configure in Claude Desktop, add to `claude_desktop_config.json`: +```json +{ + "mcpServers": { + "graphify": { + "command": "python3", + "args": ["-m", "graphify.serve", "/absolute/path/to/graphify-out/graph.json"] + } + } +} +``` + +### Step 8 - Token reduction benchmark (only if total_words > 5000) + +If `total_words` from `.graphify_detect.json` is greater than 5,000, run: + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from graphify.benchmark import run_benchmark, print_benchmark +from pathlib import Path + +detection = json.loads(Path('.graphify_detect.json').read_text()) +result = run_benchmark('graphify-out/graph.json', corpus_words=detection['total_words']) +print_benchmark(result) +" +``` + +Print the output directly in chat. If `total_words <= 5000`, skip silently - the graph value is structural clarity, not token compression, for small corpora. + +--- + +### Step 9 - Save manifest, update cost tracker, clean up, and report + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +from datetime import datetime, timezone +from graphify.detect import save_manifest + +# Save manifest for --update +detect = json.loads(Path('.graphify_detect.json').read_text()) +save_manifest(detect['files']) + +# Update cumulative cost tracker +extract = json.loads(Path('.graphify_extract.json').read_text()) +input_tok = extract.get('input_tokens', 0) +output_tok = extract.get('output_tokens', 0) + +cost_path = Path('graphify-out/cost.json') +if cost_path.exists(): + cost = json.loads(cost_path.read_text()) +else: + cost = {'runs': [], 'total_input_tokens': 0, 'total_output_tokens': 0} + +cost['runs'].append({ + 'date': datetime.now(timezone.utc).isoformat(), + 'input_tokens': input_tok, + 'output_tokens': output_tok, + 'files': detect.get('total_files', 0), +}) +cost['total_input_tokens'] += input_tok +cost['total_output_tokens'] += output_tok +cost_path.write_text(json.dumps(cost, indent=2)) + +print(f'This run: {input_tok:,} input tokens, {output_tok:,} output tokens') +print(f'All time: {cost[\"total_input_tokens\"]:,} input, {cost[\"total_output_tokens\"]:,} output ({len(cost[\"runs\"])} runs)') +" +rm -f .graphify_detect.json .graphify_extract.json .graphify_ast.json .graphify_semantic.json .graphify_analysis.json .graphify_labels.json .graphify_chunk_*.json +rm -f graphify-out/.needs_update 2>/dev/null || true +``` + +Tell the user (omit the obsidian line unless --obsidian was given): +``` +Graph complete. Outputs in PATH_TO_DIR/graphify-out/ + + graph.html - interactive graph, open in browser + GRAPH_REPORT.md - audit report + graph.json - raw graph data + obsidian/ - Obsidian vault (only if --obsidian was given) +``` + +If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi + +Replace PATH_TO_DIR with the actual absolute path of the directory that was processed. + +Then paste these sections from GRAPH_REPORT.md directly into the chat: +- God Nodes +- Surprising Connections +- Suggested Questions + +Do NOT paste the full report - just those three sections. Keep it concise. + +Then immediately offer to explore. Pick the single most interesting suggested question from the report - the one that crosses the most community boundaries or has the most surprising bridge node - and ask: + +> "The most interesting question this graph can answer: **[question]**. Want me to trace it?" + +If the user says yes, run `/graphify query "[question]"` on the graph and walk them through the answer using the graph structure - which nodes connect, which community boundaries get crossed, what the path reveals. Keep going as long as they want to explore. Each answer should end with a natural follow-up ("this connects to X - want to go deeper?") so the session feels like navigation, not a one-shot report. + +The graph is the map. Your job after the pipeline is to be the guide. + +--- + +## For --update (incremental re-extraction) + +Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.detect import detect_incremental, save_manifest +from pathlib import Path + +result = detect_incremental(Path('INPUT_PATH')) +new_total = result.get('new_total', 0) +print(json.dumps(result, indent=2)) +Path('.graphify_incremental.json').write_text(json.dumps(result)) +deleted = list(result.get('deleted_files', [])) +if new_total == 0 and not deleted: + print('No files changed since last run. Nothing to update.') + raise SystemExit(0) +if deleted: + print(f'{len(deleted)} deleted file(s) to prune.') +if new_total > 0: + print(f'{new_total} new/changed file(s) to re-extract.') +" +``` + +If new files exist, first check whether all changed files are code files: + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path + +result = json.loads(open('.graphify_incremental.json').read()) if Path('.graphify_incremental.json').exists() else {} +code_exts = {'.py','.ts','.js','.go','.rs','.java','.cpp','.c','.rb','.swift','.kt','.cs','.scala','.php','.cc','.cxx','.hpp','.h','.kts'} +new_files = result.get('new_files', {}) +all_changed = [f for files in new_files.values() for f in files] +code_only = all(Path(f).suffix.lower() in code_exts for f in all_changed) +print('code_only:', code_only) +" +``` + +If `code_only` is True: print `[graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed)`, run only Step 3A (AST) on the changed files, skip Step 3B entirely (no subagents), then go straight to merge and Steps 4โ€“8. + +If `code_only` is False (any changed file is a doc/paper/image): run the full Steps 3Aโ€“3C pipeline as normal. + + +If no new files exist (only deletions), create an empty extraction so the merge step can prune: + +```bash +if [ ! -f graphify-out/.graphify_extract.json ]; then + echo '[graphify update] Only deletions -- creating empty extraction for merge.' + $(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8') +" +fi +``` + + +Then: + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.build import build_from_json +from graphify.export import to_json +from networkx.readwrite import json_graph +import networkx as nx +from pathlib import Path + +# Load existing graph +existing_data = json.loads(Path('graphify-out/graph.json').read_text()) +G_existing = json_graph.node_link_graph(existing_data, edges='links') + +# Load new extraction +new_extraction = json.loads(Path('.graphify_extract.json').read_text()) +G_new = build_from_json(new_extraction) + +# Merge: new nodes/edges into existing graph +G_existing.update(G_new) +print(f'Merged: {G_existing.number_of_nodes()} nodes, {G_existing.number_of_edges()} edges') +" +``` + +Then run Steps 4โ€“8 on the merged graph as normal. + +After Step 4, show the graph diff: + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from graphify.analyze import graph_diff +from graphify.build import build_from_json +from networkx.readwrite import json_graph +import networkx as nx +from pathlib import Path + +# Load old graph (before update) from backup written before merge +old_data = json.loads(Path('.graphify_old.json').read_text()) if Path('.graphify_old.json').exists() else None +new_extract = json.loads(Path('.graphify_extract.json').read_text()) +G_new = build_from_json(new_extract) + +if old_data: + G_old = json_graph.node_link_graph(old_data, edges='links') + diff = graph_diff(G_old, G_new) + print(diff['summary']) + if diff['new_nodes']: + print('New nodes:', ', '.join(n['label'] for n in diff['new_nodes'][:5])) + if diff['new_edges']: + print('New edges:', len(diff['new_edges'])) +" +``` + +Before the merge step, save the old graph: `cp graphify-out/graph.json .graphify_old.json` +Clean up after: `rm -f .graphify_old.json` + +--- + +## For --cluster-only + +Skip Steps 1โ€“3. Load the existing graph from `graphify-out/graph.json` and re-run clustering: + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.cluster import cluster, score_all +from graphify.analyze import god_nodes, surprising_connections +from graphify.report import generate +from graphify.export import to_json +from networkx.readwrite import json_graph +import networkx as nx +from pathlib import Path + +data = json.loads(Path('graphify-out/graph.json').read_text()) +G = json_graph.node_link_graph(data, edges='links') + +detection = {'total_files': 0, 'total_words': 99999, 'needs_graph': True, 'warning': None, + 'files': {'code': [], 'document': [], 'paper': []}} +tokens = {'input': 0, 'output': 0} + +communities = cluster(G) +cohesion = score_all(G, communities) +gods = god_nodes(G) +surprises = surprising_connections(G, communities) +labels = {cid: 'Community ' + str(cid) for cid in communities} + +report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, '.') +Path('graphify-out/GRAPH_REPORT.md').write_text(report) +to_json(G, communities, 'graphify-out/graph.json') + +analysis = { + 'communities': {str(k): v for k, v in communities.items()}, + 'cohesion': {str(k): v for k, v in cohesion.items()}, + 'gods': gods, + 'surprises': surprises, +} +Path('.graphify_analysis.json').write_text(json.dumps(analysis, indent=2)) +print(f'Re-clustered: {len(communities)} communities') +" +``` + +Then run Steps 5โ€“9 as normal (label communities, generate viz, benchmark, clean up, report). + +--- + +## For /graphify query + +Two traversal modes - choose based on the question: + +| Mode | Flag | Best for | +|------|------|----------| +| BFS (default) | _(none)_ | "What is X connected to?" - broad context, nearest neighbors first | +| DFS | `--dfs` | "How does X reach Y?" - trace a specific chain or dependency path | + +First check the graph exists: +```bash +$(cat graphify-out/.graphify_python) -c " +from pathlib import Path +if not Path('graphify-out/graph.json').exists(): + print('ERROR: No graph found. Run /graphify first to build the graph.') + raise SystemExit(1) +" +``` +If it fails, stop and tell the user to run `/graphify ` first. + +Load `graphify-out/graph.json`, then: + +1. Find the 1-3 nodes whose label best matches key terms in the question. +2. Run the appropriate traversal from each starting node. +3. Read the subgraph - node labels, edge relations, confidence tags, source locations. +4. Answer using **only** what the graph contains. Quote `source_location` when citing a specific fact. +5. If the graph lacks enough information, say so - do not hallucinate edges. + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from networkx.readwrite import json_graph +import networkx as nx +from pathlib import Path + +data = json.loads(Path('graphify-out/graph.json').read_text()) +G = json_graph.node_link_graph(data, edges='links') + +question = 'QUESTION' +mode = 'MODE' # 'bfs' or 'dfs' +terms = [t.lower() for t in question.split() if len(t) > 3] + +# Find best-matching start nodes +scored = [] +for nid, ndata in G.nodes(data=True): + label = ndata.get('label', '').lower() + score = sum(1 for t in terms if t in label) + if score > 0: + scored.append((score, nid)) +scored.sort(reverse=True) +start_nodes = [nid for _, nid in scored[:3]] + +if not start_nodes: + print('No matching nodes found for query terms:', terms) + sys.exit(0) + +subgraph_nodes = set() +subgraph_edges = [] + +if mode == 'dfs': + # DFS: follow one path as deep as possible before backtracking. + # Depth-limited to 6 to avoid traversing the whole graph. + visited = set() + stack = [(n, 0) for n in reversed(start_nodes)] + while stack: + node, depth = stack.pop() + if node in visited or depth > 6: + continue + visited.add(node) + subgraph_nodes.add(node) + for neighbor in G.neighbors(node): + if neighbor not in visited: + stack.append((neighbor, depth + 1)) + subgraph_edges.append((node, neighbor)) +else: + # BFS: explore all neighbors layer by layer up to depth 3. + frontier = set(start_nodes) + subgraph_nodes = set(start_nodes) + for _ in range(3): + next_frontier = set() + for n in frontier: + for neighbor in G.neighbors(n): + if neighbor not in subgraph_nodes: + next_frontier.add(neighbor) + subgraph_edges.append((n, neighbor)) + subgraph_nodes.update(next_frontier) + frontier = next_frontier + +# Token-budget aware output: rank by relevance, cut at budget (~4 chars/token) +token_budget = BUDGET # default 2000 +char_budget = token_budget * 4 + +# Score each node by term overlap for ranked output +def relevance(nid): + label = G.nodes[nid].get('label', '').lower() + return sum(1 for t in terms if t in label) + +ranked_nodes = sorted(subgraph_nodes, key=relevance, reverse=True) + +lines = [f'Traversal: {mode.upper()} | Start: {[G.nodes[n].get(\"label\",n) for n in start_nodes]} | {len(subgraph_nodes)} nodes'] +for nid in ranked_nodes: + d = G.nodes[nid] + lines.append(f' NODE {d.get(\"label\", nid)} [src={d.get(\"source_file\",\"\")} loc={d.get(\"source_location\",\"\")}]') +for u, v in subgraph_edges: + if u in subgraph_nodes and v in subgraph_nodes: + _raw = G[u][v]; d = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw + lines.append(f' EDGE {G.nodes[u].get(\"label\",u)} --{d.get(\"relation\",\"\")} [{d.get(\"confidence\",\"\")}]--> {G.nodes[v].get(\"label\",v)}') + +output = '\n'.join(lines) +if len(output) > char_budget: + output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)' +print(output) +" +``` + +Replace `QUESTION` with the user's actual question, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above. + +After writing the answer, save it back into the graph so it improves future queries: + +```bash +$(cat graphify-out/.graphify_python) -m graphify save-result --question "QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +``` + +Replace `QUESTION` with the question, `ANSWER` with your full answer text, `SOURCE_NODES` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. + +--- + +## For /graphify path + +Find the shortest path between two named concepts in the graph. + +First check the graph exists: +```bash +$(cat graphify-out/.graphify_python) -c " +from pathlib import Path +if not Path('graphify-out/graph.json').exists(): + print('ERROR: No graph found. Run /graphify first to build the graph.') + raise SystemExit(1) +" +``` +If it fails, stop and tell the user to run `/graphify ` first. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json, sys +import networkx as nx +from networkx.readwrite import json_graph +from pathlib import Path + +data = json.loads(Path('graphify-out/graph.json').read_text()) +G = json_graph.node_link_graph(data, edges='links') + +a_term = 'NODE_A' +b_term = 'NODE_B' + +def find_node(term): + term = term.lower() + scored = sorted( + [(sum(1 for w in term.split() if w in G.nodes[n].get('label','').lower()), n) + for n in G.nodes()], + reverse=True + ) + return scored[0][1] if scored and scored[0][0] > 0 else None + +src = find_node(a_term) +tgt = find_node(b_term) + +if not src or not tgt: + print(f'Could not find nodes matching: {a_term!r} or {b_term!r}') + sys.exit(0) + +try: + path = nx.shortest_path(G, src, tgt) + print(f'Shortest path ({len(path)-1} hops):') + for i, nid in enumerate(path): + label = G.nodes[nid].get('label', nid) + if i < len(path) - 1: + _raw = G[nid][path[i+1]]; edge = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw + rel = edge.get('relation', '') + conf = edge.get('confidence', '') + print(f' {label} --{rel}--> [{conf}]') + else: + print(f' {label}') +except nx.NetworkXNoPath: + print(f'No path found between {a_term!r} and {b_term!r}') +except nx.NodeNotFound as e: + print(f'Node not found: {e}') +" +``` + +Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant. + +After writing the explanation, save it back: + +```bash +$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +``` + +--- + +## For /graphify explain + +Give a plain-language explanation of a single node - everything connected to it. + +First check the graph exists: +```bash +$(cat graphify-out/.graphify_python) -c " +from pathlib import Path +if not Path('graphify-out/graph.json').exists(): + print('ERROR: No graph found. Run /graphify first to build the graph.') + raise SystemExit(1) +" +``` +If it fails, stop and tell the user to run `/graphify ` first. + +```bash +$(cat graphify-out/.graphify_python) -c " +import json, sys +import networkx as nx +from networkx.readwrite import json_graph +from pathlib import Path + +data = json.loads(Path('graphify-out/graph.json').read_text()) +G = json_graph.node_link_graph(data, edges='links') + +term = 'NODE_NAME' +term_lower = term.lower() + +# Find best matching node +scored = sorted( + [(sum(1 for w in term_lower.split() if w in G.nodes[n].get('label','').lower()), n) + for n in G.nodes()], + reverse=True +) +if not scored or scored[0][0] == 0: + print(f'No node matching {term!r}') + sys.exit(0) + +nid = scored[0][1] +data_n = G.nodes[nid] +print(f'NODE: {data_n.get(\"label\", nid)}') +print(f' source: {data_n.get(\"source_file\",\"unknown\")}') +print(f' type: {data_n.get(\"file_type\",\"unknown\")}') +print(f' degree: {G.degree(nid)}') +print() +print('CONNECTIONS:') +for neighbor in G.neighbors(nid): + _raw = G[nid][neighbor]; edge = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw + nlabel = G.nodes[neighbor].get('label', neighbor) + rel = edge.get('relation', '') + conf = edge.get('confidence', '') + src_file = G.nodes[neighbor].get('source_file', '') + print(f' --{rel}--> {nlabel} [{conf}] ({src_file})') +" +``` + +Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. + +After writing the explanation, save it back: + +```bash +$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +``` + +--- + +## For /graphify add + +Fetch a URL and add it to the corpus, then update the graph. + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys +from graphify.ingest import ingest +from pathlib import Path + +try: + out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + print(f'Saved to {out}') +except ValueError as e: + print(f'error: {e}', file=sys.stderr) + sys.exit(1) +except RuntimeError as e: + print(f'error: {e}', file=sys.stderr) + sys.exit(1) +" +``` + +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. + +Supported URL types (auto-detected): +- Twitter/X โ†’ fetched via oEmbed, saved as `.md` with tweet text and author +- arXiv โ†’ abstract + metadata saved as `.md` +- PDF โ†’ downloaded as `.pdf` +- Images (.png/.jpg/.webp) โ†’ downloaded, vision extraction runs on next build +- Any webpage โ†’ converted to markdown via html2text + +--- + +## For --watch + +Start a background watcher that monitors a folder and auto-updates the graph when files change. + +```bash +python3 -m graphify.watch INPUT_PATH --debounce 3 +``` + +Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: + +- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically. +- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required). + +Debounce (default 3s): waits until file activity stops before triggering, so a wave of parallel agent writes doesn't trigger a rebuild per file. + +Press Ctrl+C to stop. + +For agentic workflows: run `--watch` in a background terminal. Code changes from agent waves are picked up automatically between waves. If agents are also writing docs or notes, you'll need a manual `/graphify --update` after those waves. + +--- + +## For git commit hook + +Install a post-commit hook that auto-rebuilds the graph after every commit. No background process needed - triggers once per commit, works with any editor. + +```bash +graphify hook install # install +graphify hook uninstall # remove +graphify hook status # check +``` + +After every `git commit`, the hook detects which code files changed (via `git diff HEAD~1`), re-runs AST extraction on those files, and rebuilds `graph.json` and `GRAPH_REPORT.md`. Doc/image changes are ignored by the hook - run `/graphify --update` manually for those. + +If a post-commit hook already exists, graphify appends to it rather than replacing it. + +--- + +## For native CLAUDE.md integration + +Run once per project to make graphify always-on in Claude Code sessions: + +```bash +graphify claude install +``` + +This writes a `## graphify` section to the local `CLAUDE.md` that instructs Claude to check the graph before answering codebase questions and rebuild it after code changes. No manual `/graphify` needed in future sessions. + +```bash +graphify claude uninstall # remove the section +``` + +--- + +## Honesty Rules + +- Never invent an edge. If unsure, use AMBIGUOUS. +- Never skip the corpus check warning. +- Always show token cost in the report. +- Never hide cohesion scores behind symbols - show the raw number. +- Never run HTML viz on a graph with more than 5,000 nodes without warning the user. diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 00000000..146af7eb --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,2 @@ +[features] +hooks = true diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 00000000..774c85c0 --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,28 @@ +{ + "hooks": { + "PreToolUse": [ + { + "hooks": [ + { + "command": "uv run graphify hook-check", + "type": "command" + } + ], + "matcher": "Bash" + } + ], + "SessionStart": [ + { + "hooks": [ + { + "command": "echo 'CAVEMAN MODE ACTIVE. Rules: Drop articles/filler/pleasantries/hedging. Fragments OK. Short synonyms. Pattern: [thing] [action] [reason]. [next step]. Not: Sure! I would be happy to help you with that. Yes: Bug in auth middleware. Fix: Code/commits/security: write normal. User says stop caveman or normal mode to deactivate.'", + "statusMessage": "Loading caveman mode", + "timeout": 5, + "type": "command" + } + ], + "matcher": "startup|resume" + } + ] + } +} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 35fd1ca8..e9b09ec0 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -12,10 +12,10 @@ RUN env UV_UNMANAGED_INSTALL="/usr/local/bin" sh /tmp/uv-installer.sh \ RUN rm -f /etc/apt/sources.list.d/yarn.list \ && apt-get update \ && apt-get install -y --no-install-recommends \ - bubblewrap \ - socat \ - curl \ - direnv \ + bubblewrap \ + socat \ + curl \ + direnv \ && rm -rf /var/lib/apt/lists/* # Install 1Password CLI (Added by Michaรซl: 7-5-2026) @@ -25,6 +25,14 @@ RUN curl -sS https://downloads.1password.com/linux/keys/1password.asc | gpg --de && apt-get install -y --no-install-recommends 1password-cli \ && rm -rf /var/lib/apt/lists/* +# Codex CLI ships as an npm package and requires Node.js 22+. +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get update \ + && apt-get install -y --no-install-recommends nodejs \ + && npm install -g @openai/codex \ + && codex --version \ + && rm -rf /var/lib/apt/lists/* + ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ PYTHONDONTWRITEBYTECODE=1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b91c285c..ae5eac40 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -28,8 +28,8 @@ "docker.docker", "dbaeumer.vscode-eslint", "tamasfe.even-better-toml", - "cweijan.vscode-excel", - "GitHub.copilot-chat", + "cweijan.vscode-office", + "openai.chatgpt", "github.vscode-github-actions", "GitHub.vscode-pull-request-github", "ms-azuretools.vscode-azure-github-copilot", @@ -43,7 +43,7 @@ "settings": { "jupyter.jupyterServerType": "local", "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", - "python.terminal.activateEnvironment": true, + "python.terminal.activateEnvironment": false, "python.testing.pytestArgs": [ "." ], @@ -56,10 +56,10 @@ "mounts": [ "source=python-template-uv-cache,target=/home/vscode/.cache/uv,type=volume", "source=python-template-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume", - "source=${localEnv:HOME},target=/host-home,type=bind,consistency=cached" + "source=${localEnv:HOME}/.codex,target=/home/vscode/.codex,type=bind,consistency=cached" ], "name": "python-basic-template", - "postCreateCommand": "bash .devcontainer/post-create.sh", + "postCreateCommand": "bash ${containerWorkspaceFolder}/.devcontainer/post-create.sh", "remoteUser": "vscode", "updateRemoteUserUID": true, "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}" diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index a58f63db..f3feaeaf 100644 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -4,25 +4,6 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${repo_root}" -# Expose the host ~/.copilot directory inside the devcontainer. -# This assumes devcontainer.json mounts the host home directory to /host-home. -if [[ -d /host-home ]]; then - mkdir -p /host-home/.copilot - - container_copilot_dir="${HOME}/.copilot" - host_copilot_dir="/host-home/.copilot" - - if [[ -L "${container_copilot_dir}" ]]; then - echo "Copilot directory symlink already exists at ${container_copilot_dir}." - elif [[ -e "${container_copilot_dir}" ]]; then - echo "Copilot directory already exists at ${container_copilot_dir}; not replacing it." - echo "To use the host Copilot directory, move this folder aside and link it to ${host_copilot_dir}." - else - ln -s "${host_copilot_dir}" "${container_copilot_dir}" - echo "Linked ${container_copilot_dir} to ${host_copilot_dir}." - fi -else - echo "Host home mount /host-home not found; skipping Copilot directory link." fi echo "Bootstrapping the container workspace with uv..." @@ -31,6 +12,7 @@ python --version # uv venv .venv uv sync --frozen --all-groups +uv tool install graphifyy # (Added by Michaรซl: 7-5-2026) # load environment variables from .envrc diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4355965e..1c69c036 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -15,8 +15,8 @@ "docker.docker", "dbaeumer.vscode-eslint", "tamasfe.even-better-toml", - "cweijan.vscode-excel", - "GitHub.copilot-chat", + "cweijan.vscode-office", + "openai.chatgpt", "github.vscode-github-actions", "GitHub.vscode-pull-request-github", "ms-azuretools.vscode-azure-github-copilot", diff --git a/uv.lock b/uv.lock index 1cf17a4c..d83411d7 100644 --- a/uv.lock +++ b/uv.lock @@ -542,11 +542,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, ] [[package]] From 1788a3481e26db0cba530fee45760ae9f20e5c77 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 1 Jun 2026 11:30:40 +0000 Subject: [PATCH 146/199] fix: clean up post-create script and enhance VSCode settings for better formatting and linting --- .devcontainer/post-create.sh | 2 - .vscode/settings.json | 84 +++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index f3feaeaf..719a826a 100644 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -4,8 +4,6 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${repo_root}" -fi - echo "Bootstrapping the container workspace with uv..." uv --version python --version diff --git a/.vscode/settings.json b/.vscode/settings.json index e6b2045f..57dc60e0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,70 @@ { + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true + }, + "[javascriptreact]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true + }, + "[json]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true + }, + "[python]": { + "editor.codeActionsOnSave": { + "source.fixAll.ruff": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true + }, + "[typescriptreact]": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true + }, + "chatgpt.openOnStartup": true, + "chatgpt.runCodexInWindowsSubsystemForLinux": true, + "debug.internalConsoleOptions": "neverOpen", + "debug.showProtocolLogs": false, + "editor.formatOnPaste": true, + "editor.rulers": [ + 88 + ], + "editor.trimAutoWhitespace": true, + "eslint.format.enable": true, + "eslint.useFlatConfig": true, + "eslint.workingDirectories": [ + { + "pattern": "web" + } + ], + "files.exclude": { + "**/.pytest_cache": true, + "**/.ruff_cache": true, + "**/__pycache__": true, + "**/node_modules": true + }, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.pytest_cache": true, + "**/.ruff_cache": true, + "**/__pycache__": true, + "**/node_modules": true + }, + "git.ignoreLimitWarning": true, + "js/ts.tsdk.path": "${workspaceFolder}/web/node_modules/typescript/lib", + "js/ts.tsdk.promptToUseWorkspaceVersion": true, "mypy-type-checker.args": [ "--config-file=pyproject.toml" ], @@ -7,10 +73,26 @@ "mypy-type-checker.reportingScope": "workspace", "python.analysis.typeCheckingMode": "standard", "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.terminal.activateEnvInCurrentTerminal": false, + "python.terminal.activateEnvironment": false, "python.testing.pytestArgs": [ "." ], "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, - "ruff.enable": true + "ruff.enable": true, + "search.exclude": { + "**/.git": true, + "**/.pytest_cache": true, + "**/.venv": true, + "**/__pycache__": true, + "**/build": true, + "**/dist": true, + "**/node_modules": true + }, + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.env.linux": { + "PYTHONPATH": "${workspaceFolder}", + "UV_PROJECT_ENVIRONMENT": "${workspaceFolder}/.venv" + } } From 9a28b81593df1d2fecbba2c48046152b1bc27c82 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 1 Jun 2026 11:32:04 +0000 Subject: [PATCH 147/199] fix: update permissions for run-pip-audit script to make it executable --- scripts/hooks/run-pip-audit.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/hooks/run-pip-audit.sh diff --git a/scripts/hooks/run-pip-audit.sh b/scripts/hooks/run-pip-audit.sh old mode 100644 new mode 100755 From 7c0a2c145267d4a2d877d6b9ef1ef5fc38933cfd Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 1 Jun 2026 11:35:10 +0000 Subject: [PATCH 148/199] fix: add step to prepare Codex config mount in CI workflow --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8299b912..07ff4b54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,9 @@ jobs: - name: Checkout code uses: actions/checkout@v6 + - name: Prepare Codex config mount + run: mkdir -p "$HOME/.codex" + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From db2d9812a8ff4347d45be4221c7e8e33c403edd8 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 1 Jun 2026 11:49:30 +0000 Subject: [PATCH 149/199] fix: add Docker outside of Docker feature to devcontainer configuration --- .devcontainer/devcontainer-lock.json | 9 +++++++++ .devcontainer/devcontainer.json | 4 ++++ 2 files changed, 13 insertions(+) create mode 100644 .devcontainer/devcontainer-lock.json diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 00000000..717056d0 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,9 @@ +{ + "features": { + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { + "integrity": "sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e", + "resolved": "ghcr.io/devcontainers/features/docker-outside-of-docker@sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e", + "version": "1.10.0" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ae5eac40..a81cf4bc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -53,12 +53,16 @@ } } }, + "features": { + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} + }, "mounts": [ "source=python-template-uv-cache,target=/home/vscode/.cache/uv,type=volume", "source=python-template-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume", "source=${localEnv:HOME}/.codex,target=/home/vscode/.codex,type=bind,consistency=cached" ], "name": "python-basic-template", + "overrideCommand": true, "postCreateCommand": "bash ${containerWorkspaceFolder}/.devcontainer/post-create.sh", "remoteUser": "vscode", "updateRemoteUserUID": true, From 635a8a8283d4a620d98d4378899b40df2cff9bb3 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 3 Jun 2026 09:29:19 +0000 Subject: [PATCH 150/199] feat: add workflow to enforce PRs from 'staging' to 'main' --- .github/workflows/staging-main-check.yml | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/staging-main-check.yml diff --git a/.github/workflows/staging-main-check.yml b/.github/workflows/staging-main-check.yml new file mode 100644 index 00000000..8b5e7247 --- /dev/null +++ b/.github/workflows/staging-main-check.yml @@ -0,0 +1,34 @@ +name: Allow only staging โ†’ main + +on: + pull_request: + types: [opened, synchronize, reopened, edited, ready_for_review] + branches: [main] + +permissions: + contents: read + pull-requests: read + +# This workflow enforces the rule: only PRs where the head/source branch is 'staging' +# are allowed to target 'main'. It prevents developers from opening PRs directly +# from feature branches into main; they must go via 'staging'. + +jobs: + gate: + name: Gate source branch + runs-on: ubuntu-latest + steps: + - name: Fail if source is not 'staging' + run: | + SRC="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + echo "Base: $BASE | Source: $SRC" + if [ "$BASE" != "main" ]; then + echo "Not targeting main; skipping." + exit 0 + fi + if [ "$SRC" != "staging" ]; then + echo "::error::Only PRs from 'staging' into 'main' are allowed. Current source: '$SRC'." + exit 1 + fi + echo "staging โ†’ main allowed โœ…" From 0074d057fbac378016536e10e2caf5dc88552191 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 3 Jun 2026 09:47:35 +0000 Subject: [PATCH 151/199] fix: update Dockerfile and devcontainer configuration for improved environment setup --- .devcontainer/Dockerfile | 11 ++++++----- .devcontainer/devcontainer.json | 5 +++-- .devcontainer/post-create.sh | 20 +++++++++----------- .env.template | 1 + .envrc | 15 --------------- .gitignore | 1 + .secret.baseline | 14 ++++++++++++-- 7 files changed, 32 insertions(+), 35 deletions(-) create mode 100644 .env.template delete mode 100644 .envrc diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index e9b09ec0..f0e7c612 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -8,14 +8,15 @@ ADD https://astral.sh/uv/${UV_VERSION}/install.sh /tmp/uv-installer.sh RUN env UV_UNMANAGED_INSTALL="/usr/local/bin" sh /tmp/uv-installer.sh \ && rm /tmp/uv-installer.sh -# Install direnv (Added by Michaรซl: 7-5-2026) RUN rm -f /etc/apt/sources.list.d/yarn.list \ && apt-get update \ && apt-get install -y --no-install-recommends \ - bubblewrap \ - socat \ - curl \ - direnv \ + libpq-dev \ + netcat-openbsd \ + postgresql-client \ + redis-tools \ + bubblewrap \ + socat \ && rm -rf /var/lib/apt/lists/* # Install 1Password CLI (Added by Michaรซl: 7-5-2026) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a81cf4bc..ea227512 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,9 +5,10 @@ }, "containerEnv": { "PRE_COMMIT_HOME": "/home/vscode/.cache/pre-commit", + "PYTHONPATH": "${workspaceFolder}", "UV_CACHE_DIR": "/home/vscode/.cache/uv", "UV_LINK_MODE": "copy", - "UV_PROJECT_ENVIRONMENT": ".venv", + "UV_PROJECT_ENVIRONMENT": "${workspaceFolder}/.venv", "UV_PYTHON_DOWNLOADS": "never" }, "customizations": { @@ -63,7 +64,7 @@ ], "name": "python-basic-template", "overrideCommand": true, - "postCreateCommand": "bash ${containerWorkspaceFolder}/.devcontainer/post-create.sh", + "postCreateCommand": "bash .devcontainer/post-create.sh", "remoteUser": "vscode", "updateRemoteUserUID": true, "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}" diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 719a826a..b0591028 100644 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -4,6 +4,15 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${repo_root}" +if [[ ! -f .env && -f .env.template ]]; then + cp .env.template .env + echo "Created .env from .env.template for local devcontainer startup." +elif [[ -f .env ]]; then + source .env +else + echo "No .env file found and no .env.template to copy from. Please create a .env file based on the .env.template for local development." +fi + echo "Bootstrapping the container workspace with uv..." uv --version python --version @@ -12,17 +21,6 @@ python --version uv sync --frozen --all-groups uv tool install graphifyy -# (Added by Michaรซl: 7-5-2026) -# load environment variables from .envrc -source .envrc - -# (Added by Michaรซl: 7-5-2026) -# ensure .envrc is sourced automatically when opening new terminals inside the container -if ! grep -q "source .envrc" ~/.bashrc 2>/dev/null; then - echo "# Auto-source .envrc for this workspace" >> ~/.bashrc - echo "if [[ -f \"${repo_root}/.envrc\" ]]; then source \"${repo_root}/.envrc\"; fi" >> ~/.bashrc -fi - if [[ -d .git ]]; then uv run pre-commit install --install-hooks fi diff --git a/.env.template b/.env.template new file mode 100644 index 00000000..dd02960d --- /dev/null +++ b/.env.template @@ -0,0 +1 @@ +TEST_SECRET='test_secret_value' diff --git a/.envrc b/.envrc deleted file mode 100644 index 1ee6f0cb..00000000 --- a/.envrc +++ /dev/null @@ -1,15 +0,0 @@ -# Load environment variables from .env if it exists -if [ -f .env ]; then - set -a - source .env - set +a -fi - -export PYTHONPATH=. -export VENV_DIR=".venv" -export PYPROJECT="pyproject.toml" - -# Set a virtualenv automatically if venv exists -if [ -d "$VENV_DIR" ]; then - source $VENV_DIR/bin/activate -fi diff --git a/.gitignore b/.gitignore index 48a2be52..4f713121 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,7 @@ pip-delete-this-directory.txt # dotenv files (if used for secrets) .env .env.* +!.env.template # Poetry poetry.lock diff --git a/.secret.baseline b/.secret.baseline index f3a59ef3..0a160355 100644 --- a/.secret.baseline +++ b/.secret.baseline @@ -92,7 +92,7 @@ }, { "path": "detect_secrets.filters.common.is_baseline_file", - "filename": ".secret.baseline" + "filename": "./.secret.baseline" }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", @@ -127,6 +127,16 @@ } ], "results": { + ".env.template": [ + { + "type": "Secret Keyword", + "filename": ".env.template", + "hashed_secret": "370f854b604ba6a825818b745c2708b73f07c01c", + "is_verified": false, + "line_number": 1, + "is_secret": false + } + ], "tests/utils/test_secrets.py": [ { "type": "Secret Keyword", @@ -146,5 +156,5 @@ } ] }, - "generated_at": "2026-03-03T13:51:03Z" + "generated_at": "2026-06-03T09:46:58Z" } From 345cc0331a73fb419603ea305a57ef673b2e0c0f Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 3 Jun 2026 09:49:34 +0000 Subject: [PATCH 152/199] fix: update VSCode extensions for improved development experience --- .devcontainer/devcontainer.json | 10 +++++++--- .vscode/extensions.json | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ea227512..d2d0675d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,6 +22,7 @@ "charliermarsh.ruff", "ms-toolsai.jupyter", "astral-sh.ty", + "charliermarsh.ruff", "ms-python.autopep8", "ms-python.black-formatter", "ms-python.debugpy", @@ -29,8 +30,8 @@ "docker.docker", "dbaeumer.vscode-eslint", "tamasfe.even-better-toml", - "cweijan.vscode-office", - "openai.chatgpt", + "cweijan.vscode-excel", + "GitHub.copilot-chat", "github.vscode-github-actions", "GitHub.vscode-pull-request-github", "ms-azuretools.vscode-azure-github-copilot", @@ -39,7 +40,10 @@ "mtxr.sqltools-driver-pg", "mtxr.sqltools-driver-sqlite", "ms-ossdata.vscode-pgsql", - "formulahendry.docker-explorer" + "biomejs.biome", + "bradlc.vscode-tailwindcss", + "ms-playwright.playwright", + "me-dutour-mathieu.vscode-github-actions" ], "settings": { "jupyter.jupyterServerType": "local", diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1c69c036..0a976b09 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -8,6 +8,7 @@ "charliermarsh.ruff", "ms-toolsai.jupyter", "astral-sh.ty", + "charliermarsh.ruff", "ms-python.autopep8", "ms-python.black-formatter", "ms-python.debugpy", @@ -15,8 +16,8 @@ "docker.docker", "dbaeumer.vscode-eslint", "tamasfe.even-better-toml", - "cweijan.vscode-office", - "openai.chatgpt", + "cweijan.vscode-excel", + "GitHub.copilot-chat", "github.vscode-github-actions", "GitHub.vscode-pull-request-github", "ms-azuretools.vscode-azure-github-copilot", @@ -25,6 +26,9 @@ "mtxr.sqltools-driver-pg", "mtxr.sqltools-driver-sqlite", "ms-ossdata.vscode-pgsql", - "formulahendry.docker-explorer" + "biomejs.biome", + "bradlc.vscode-tailwindcss", + "ms-playwright.playwright", + "me-dutour-mathieu.vscode-github-actions" ] } From c408ee5c9b8650ca7848c55d559dd085d3abe250 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 3 Jun 2026 10:06:32 +0000 Subject: [PATCH 153/199] Update skills & codex files --- {.copilot => .agents}/agents/coder.agent.md | 0 .../agents/designer.agent.md | 0 .../agents/orchestrator.agent.md | 0 {.copilot => .agents}/agents/planner.agent.md | 0 .../codeql-analysis-remediation/SKILL.md | 6 + .../assets/codeql-commands.sh | 0 .../assets/report-template.md | 0 .../assets/task-delegate-template.md | 0 .../references/codeql-workflow.md | 0 .../references/safe-remediation-rules.md | 0 .../references/sarif-interpretation.md | 0 .../references/verification-commands.md | 0 .../SKILL.md | 0 .../run-pip-audit.sh | 0 .codex/hooks.json | 13 - .../audit-python-template-compliance/SKILL.md | 88 ---- .../assets/audit-report-template.md | 53 -- .../assets/config-template-manifest.txt | 23 - .../references/audit-procedure.md | 217 -------- .../references/template-baseline.md | 200 -------- .../references/template-config-settings.md | 106 ---- .devcontainer/post-create.sh | 5 +- AGENTS.md | 465 ++++++++++++++++++ 23 files changed, 474 insertions(+), 702 deletions(-) rename {.copilot => .agents}/agents/coder.agent.md (100%) rename {.copilot => .agents}/agents/designer.agent.md (100%) rename {.copilot => .agents}/agents/orchestrator.agent.md (100%) rename {.copilot => .agents}/agents/planner.agent.md (100%) rename {.copilot => .agents}/skills/codeql-analysis-remediation/SKILL.md (95%) rename {.copilot => .agents}/skills/codeql-analysis-remediation/assets/codeql-commands.sh (100%) rename {.copilot => .agents}/skills/codeql-analysis-remediation/assets/report-template.md (100%) rename {.copilot => .agents}/skills/codeql-analysis-remediation/assets/task-delegate-template.md (100%) rename {.copilot => .agents}/skills/codeql-analysis-remediation/references/codeql-workflow.md (100%) rename {.copilot => .agents}/skills/codeql-analysis-remediation/references/safe-remediation-rules.md (100%) rename {.copilot => .agents}/skills/codeql-analysis-remediation/references/sarif-interpretation.md (100%) rename {.copilot => .agents}/skills/codeql-analysis-remediation/references/verification-commands.md (100%) rename {.copilot => .agents}/skills/update-dependencies-fix-vulnerabilities/SKILL.md (100%) rename {.copilot => .agents}/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh (100%) delete mode 100644 .copilot/skills/audit-python-template-compliance/SKILL.md delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/audit-report-template.md delete mode 100644 .copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt delete mode 100644 .copilot/skills/audit-python-template-compliance/references/audit-procedure.md delete mode 100644 .copilot/skills/audit-python-template-compliance/references/template-baseline.md delete mode 100644 .copilot/skills/audit-python-template-compliance/references/template-config-settings.md create mode 100644 AGENTS.md diff --git a/.copilot/agents/coder.agent.md b/.agents/agents/coder.agent.md similarity index 100% rename from .copilot/agents/coder.agent.md rename to .agents/agents/coder.agent.md diff --git a/.copilot/agents/designer.agent.md b/.agents/agents/designer.agent.md similarity index 100% rename from .copilot/agents/designer.agent.md rename to .agents/agents/designer.agent.md diff --git a/.copilot/agents/orchestrator.agent.md b/.agents/agents/orchestrator.agent.md similarity index 100% rename from .copilot/agents/orchestrator.agent.md rename to .agents/agents/orchestrator.agent.md diff --git a/.copilot/agents/planner.agent.md b/.agents/agents/planner.agent.md similarity index 100% rename from .copilot/agents/planner.agent.md rename to .agents/agents/planner.agent.md diff --git a/.copilot/skills/codeql-analysis-remediation/SKILL.md b/.agents/skills/codeql-analysis-remediation/SKILL.md similarity index 95% rename from .copilot/skills/codeql-analysis-remediation/SKILL.md rename to .agents/skills/codeql-analysis-remediation/SKILL.md index 972663f6..786bb12e 100644 --- a/.copilot/skills/codeql-analysis-remediation/SKILL.md +++ b/.agents/skills/codeql-analysis-remediation/SKILL.md @@ -1,3 +1,9 @@ +--- +name: codeql-analysis-remediation +description: > + Analyze repository with CodeQL, identify issues, plan and implement safe fixes, verify results. Lead remediation efforts with controlled, reviewable improvements. Use when task involves CodeQL security/quality analysis and remediation. +--- + # Skill: CodeQL repository analysis and remediation orchestration ## Purpose diff --git a/.copilot/skills/codeql-analysis-remediation/assets/codeql-commands.sh b/.agents/skills/codeql-analysis-remediation/assets/codeql-commands.sh similarity index 100% rename from .copilot/skills/codeql-analysis-remediation/assets/codeql-commands.sh rename to .agents/skills/codeql-analysis-remediation/assets/codeql-commands.sh diff --git a/.copilot/skills/codeql-analysis-remediation/assets/report-template.md b/.agents/skills/codeql-analysis-remediation/assets/report-template.md similarity index 100% rename from .copilot/skills/codeql-analysis-remediation/assets/report-template.md rename to .agents/skills/codeql-analysis-remediation/assets/report-template.md diff --git a/.copilot/skills/codeql-analysis-remediation/assets/task-delegate-template.md b/.agents/skills/codeql-analysis-remediation/assets/task-delegate-template.md similarity index 100% rename from .copilot/skills/codeql-analysis-remediation/assets/task-delegate-template.md rename to .agents/skills/codeql-analysis-remediation/assets/task-delegate-template.md diff --git a/.copilot/skills/codeql-analysis-remediation/references/codeql-workflow.md b/.agents/skills/codeql-analysis-remediation/references/codeql-workflow.md similarity index 100% rename from .copilot/skills/codeql-analysis-remediation/references/codeql-workflow.md rename to .agents/skills/codeql-analysis-remediation/references/codeql-workflow.md diff --git a/.copilot/skills/codeql-analysis-remediation/references/safe-remediation-rules.md b/.agents/skills/codeql-analysis-remediation/references/safe-remediation-rules.md similarity index 100% rename from .copilot/skills/codeql-analysis-remediation/references/safe-remediation-rules.md rename to .agents/skills/codeql-analysis-remediation/references/safe-remediation-rules.md diff --git a/.copilot/skills/codeql-analysis-remediation/references/sarif-interpretation.md b/.agents/skills/codeql-analysis-remediation/references/sarif-interpretation.md similarity index 100% rename from .copilot/skills/codeql-analysis-remediation/references/sarif-interpretation.md rename to .agents/skills/codeql-analysis-remediation/references/sarif-interpretation.md diff --git a/.copilot/skills/codeql-analysis-remediation/references/verification-commands.md b/.agents/skills/codeql-analysis-remediation/references/verification-commands.md similarity index 100% rename from .copilot/skills/codeql-analysis-remediation/references/verification-commands.md rename to .agents/skills/codeql-analysis-remediation/references/verification-commands.md diff --git a/.copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md b/.agents/skills/update-dependencies-fix-vulnerabilities/SKILL.md similarity index 100% rename from .copilot/skills/update-dependencies-fix-vulnerabilities/SKILL.md rename to .agents/skills/update-dependencies-fix-vulnerabilities/SKILL.md diff --git a/.copilot/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh b/.agents/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh similarity index 100% rename from .copilot/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh rename to .agents/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh diff --git a/.codex/hooks.json b/.codex/hooks.json index 774c85c0..921623cf 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -10,19 +10,6 @@ ], "matcher": "Bash" } - ], - "SessionStart": [ - { - "hooks": [ - { - "command": "echo 'CAVEMAN MODE ACTIVE. Rules: Drop articles/filler/pleasantries/hedging. Fragments OK. Short synonyms. Pattern: [thing] [action] [reason]. [next step]. Not: Sure! I would be happy to help you with that. Yes: Bug in auth middleware. Fix: Code/commits/security: write normal. User says stop caveman or normal mode to deactivate.'", - "statusMessage": "Loading caveman mode", - "timeout": 5, - "type": "command" - } - ], - "matcher": "startup|resume" - } ] } } diff --git a/.copilot/skills/audit-python-template-compliance/SKILL.md b/.copilot/skills/audit-python-template-compliance/SKILL.md deleted file mode 100644 index d0abefea..00000000 --- a/.copilot/skills/audit-python-template-compliance/SKILL.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -name: audit-python-template-compliance -description: 'Audit a Python repository against the TECHLETES python_basic_template and bring it into compliance. Use when you need to align a repo to the template by copying the shipped config bases, review drift in pyproject/pre-commit/CI/devcontainer/docs/secrets workflow, remediate gaps, rerun the audit, validate the repo, and produce a compliance report.' -argument-hint: 'Target repository path or repo name, plus any approved exceptions from the template' -user-invocable: true ---- - -# Python Template Compliance Audit - -Use this skill to audit a target repository against the TECHLETES Python basic -template, align the repo to the template where appropriate, re-audit, run the -available validation commands, and finish with a concise audit report. - -## Use This Skill When - -- A repository should conform to the TECHLETES Python template but predates it. -- You need to detect and fix drift in setup, tooling, quality gates, docs, or - secret-handling workflows. -- You want a structured compliance pass that ends with remediation and - verification instead of a checklist only. - -## Do Not Use This Skill When - -- The target repository is not primarily a Python repository. -- The target repository is intentionally on a different baseline and the user - has not asked to migrate it toward this template. -- The task is only to review or explain differences without making changes. - -## Scope Rules - -1. Confirm the target repository and record any approved exceptions before - changing files. -2. Treat executable configuration as the source of truth when prose docs drift. -3. For config-bearing files indexed by this skill, copy the provided base file - instead of recreating it. Use files shipped directly with this skill when - they exist; otherwise use the repo-sourced assets materialized from - `assets/config-template-manifest.txt` into `assets/config-template/` at - install time, and only after that replace the approved target-specific - values. -4. Package names, repo URLs, excluded directories, cache volume names, and - secret references must match the target repository. -5. Fix root causes first. Do not patch around validation failures with - one-off exceptions unless the user explicitly approves the deviation. - -## Required References - -- Read the template baseline in [template-baseline](./references/template-baseline.md). -- Use the asset index in [template-config-settings](./references/template-config-settings.md) - as the default copy source for config files. When a matching shipped file or - materialized repo asset exists, copy it first and then make only the - approved target-specific substitutions. -- Follow the execution sequence in [audit-procedure](./references/audit-procedure.md). -- Use the final structure in [audit-report-template](./assets/audit-report-template.md). - -## Deliverables - -By the end of the run, produce all of the following when feasible: - -1. A concrete audit of the target repo against the template baseline. -2. A remediation plan for every non-compliant item. -3. Implemented fixes, executed in parallel where independence is real. -4. A second audit confirming whether the repo is now compliant. -5. A validation pass using the repo's available quality gates and startup paths. -6. A final audit report summarizing findings, changes, verification, and any - approved remaining deviations. - -## Working Style - -- Start from the closest controlling files: `pyproject.toml`, - `.pre-commit-config.yaml`, `.github/workflows/`, `.devcontainer/`, - `scripts/hooks/`, `README.md`, `docs/`, first-party packages, and test entry - points. -- When a config surface has a shipped base file or a materialized repo asset in - this skill, copy that file into place before making local adjustments. Do not - rebuild those configs from memory. -- Keep the first edits small and validate immediately. -- Parallelize only independent slices, such as docs versus CI copy edits, after - you know they do not share the same controlling config. -- If the target repo lacks a required baseline feature, add the minimal - compliant implementation and then validate it. - -## Completion Standard - -The skill is complete only after you have either: - -- Brought the repository into compliance and verified it, or -- Identified the exact blockers, documented the remaining deviations, and - reported which validations could not be completed. diff --git a/.copilot/skills/audit-python-template-compliance/assets/audit-report-template.md b/.copilot/skills/audit-python-template-compliance/assets/audit-report-template.md deleted file mode 100644 index 5dacdfca..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/audit-report-template.md +++ /dev/null @@ -1,53 +0,0 @@ -# Audit Report Template - -## Scope - -- Target repository: -- Branch: -- Expected alignment level: -- Approved deviations: - -## Baseline Summary - -- Template baseline used: -- Source-of-truth files reviewed: - -## Findings Before Remediation - -| Area | Expected | Actual | Severity | Planned action | -| --- | --- | --- | --- | --- | -| | | | | | - -## Changes Applied - -| Area | Files changed | Root-cause fix | Validation after edit | -| --- | --- | --- | --- | -| | | | | - -## Re-Audit Result - -| Area | Final state | Notes | -| --- | --- | --- | -| | Fixed / approved deviation / blocked / not applicable | | - -## Validation Run - -| Command or check | Result | Notes | -| --- | --- | --- | -| `uv sync` | | | -| `uv run pre-commit run --all-files` | | | -| `uv run pytest` | | | -| `uv run mypy . --config-file=pyproject.toml` | | | - -Add service startup checks or other repo-specific validations below the table. - -## Final Compliance Status - -- Compliant: -- Remaining deviations: -- Blockers: - -## Follow-Up - -- Immediate follow-up items: -- Optional future improvements: diff --git a/.copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt b/.copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt deleted file mode 100644 index b7ea4f1f..00000000 --- a/.copilot/skills/audit-python-template-compliance/assets/config-template-manifest.txt +++ /dev/null @@ -1,23 +0,0 @@ -# Relative repository paths materialized into assets/config-template/ at install time. -pyproject.toml -.pre-commit-config.yaml -.github/workflows/ci.yml -.github/dependabot.yml -.github/pull_request_template.md -.github/copilot-instructions.md -.github/ISSUE_TEMPLATE/config.yml -.github/ISSUE_TEMPLATE/bug-report.md -.github/ISSUE_TEMPLATE/docs_update.md -.github/ISSUE_TEMPLATE/feature-request.md -.devcontainer/devcontainer.json -.devcontainer/Dockerfile -.devcontainer/post-create.sh -.devcontainer/.dockerignore -.vscode/extensions.json -.vscode/settings.json -.envrc -.detect-secrets.yaml -.gitattributes -.gitignore -scripts/hooks/nbstripout-autoadd.sh -scripts/hooks/run-pip-audit.sh diff --git a/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md b/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md deleted file mode 100644 index 2bfef638..00000000 --- a/.copilot/skills/audit-python-template-compliance/references/audit-procedure.md +++ /dev/null @@ -1,217 +0,0 @@ -# Audit Procedure - -Follow this sequence to audit a target repository against the TECHLETES Python -template, remediate gaps, and verify the result. - -## Outcome - -Complete all of these when feasible: - -1. Capture the repo's current state against the template baseline. -2. Build a concrete remediation plan for every non-compliant item. -3. Implement fixes with minimal, root-cause edits. -4. Re-audit after changes. -5. Run the available validations and service startups. -6. Produce a final audit report. - -## Step 1. Confirm Scope and Exceptions - -Before editing: - -1. Identify the target repository and working branch. -2. Confirm whether the repo is expected to align fully or partially with the - template. -3. Record approved deviations up front. -4. If the repo is not primarily Python or does not use `uv` as its intended - package workflow, stop and ask whether this template is the correct baseline. - -## Step 2. Collect Evidence - -Read the controlling surfaces first, in parallel where possible. - -Primary evidence set: - -- `pyproject.toml` -- `uv.lock` -- `.pre-commit-config.yaml` -- `.github/workflows/` -- `.github/dependabot.yml` -- `.github/pull_request_template.md` -- `.github/ISSUE_TEMPLATE/` -- `.github/copilot-instructions.md` -- `.devcontainer/` -- `.vscode/` -- `README.md` -- `CONTRIBUTING.md` -- `docs/` -- `.envrc` -- `.detect-secrets.yaml` -- `.gitignore` -- `.gitattributes` -- secret helper modules -- package `__init__.py` files for runtime typing setup -- tests and smoke checks -- helper scripts such as setup, new-branch, hook wrappers under - `scripts/hooks/`, and audit wrappers -- Copilot assets under `.copilot/` when present - -Use `template-config-settings.md` and its shipped or materialized config files -as the default base for concrete tool settings such as pytest `addopts`, mypy -strictness flags, Ruff rule sets, Bandit skips, pre-commit hook revisions, -delegated hook scripts, and devcontainer editor settings. When a matching -shipped file or materialized repo asset exists, copy it first and then replace -only the approved target-specific values. - -If a baseline file is missing, treat that as a finding rather than continuing as -though the capability exists. - -## Step 3. Build the Audit Matrix - -Create a finding entry for each baseline area: - -- Dependency management and packaging -- Typing and runtime type enforcement -- Pre-commit hooks and security gates -- Secret handling -- CI validation -- Devcontainer support -- Repo scripts and developer workflow -- Documentation completeness and accuracy - -For each finding, record: - -- Expected baseline -- Expected concrete setting values when relevant -- Actual state -- Severity: blocking, major, minor, or informational -- Whether it is auto-fixable -- The smallest validating command or check -- Whether the gap is a real defect or an approved deviation - -## Step 4. Create the Alignment Plan - -Plan fixes in dependency order. - -Recommended order: - -1. Core project model - `pyproject.toml`, package layout, `uv.lock` -2. Quality and security gates - `.pre-commit-config.yaml`, pip-audit, Bandit, detect-secrets -3. Automation and environment setup - `scripts/`, `scripts/hooks/`, `.github/workflows/`, `.devcontainer/`, - `.envrc` -4. Code and tests - runtime typing hooks, helper modules, missing tests -5. Documentation - `README.md`, `CONTRIBUTING.md`, `docs/` - -Parallelize only independent slices. Examples: - -- Docs can often be updated in parallel with test additions after the final - executable behavior is known. -- CI and devcontainer copy edits may be parallel only if neither depends on the - same config changes still in flight. - -Do not parallelize changes that share a controlling file or a single validation -surface such as `pyproject.toml` plus `uv.lock`. - -## Step 5. Execute the Plan - -Implement the smallest set of changes that makes the target repo comply with the -baseline. - -Rules: - -1. For shipped config surfaces, copy the matching file from this skill as the - base before editing. Prefer files shipped directly with the skill, then the - repo-sourced files materialized under `assets/config-template/` from - `assets/config-template-manifest.txt`. -2. Prefer adding missing baseline capabilities over documenting gaps away. -3. If the target repo already has an equivalent mechanism, align and document it - instead of replacing it unnecessarily. -4. Keep edits reversible and validate the touched slice immediately. - -For missing or substantially divergent config files, do not rebuild them from -memory. Start from the shipped or materialized base file, then apply -target-specific substitutions. - -### Focused Validation After Each Edit Slice - -After the first substantive edit, run the cheapest meaningful check before doing -more exploration. - -Examples: - -- If `pyproject.toml` changes: run `uv lock` and then `uv sync` if needed. -- If `.pre-commit-config.yaml` changes: run the affected hook or - `uv run pre-commit run --all-files` if narrow hook execution is awkward. -- If Python source changes: run the nearest relevant test, then mypy if the - change affects typing. -- If docs change only: executable validation is not available, so verify the - docs against the final config and move on. - -If a validation fails, repair that same slice first. Do not widen scope until -the current slice is understood. - -## Step 6. Re-Audit - -Re-run the same compliance matrix after the fixes land. - -Mark each finding as one of: - -- Fixed -- Approved deviation -- Blocked -- Not applicable - -The re-audit is not complete until every original finding has one of those -states. - -## Step 7. Full Verification - -Run the target repo's available validation commands. For repos aligned to this -template, the standard sequence is: - -```bash -uv sync -uv run pre-commit run --all-files -uv run pytest -uv run mypy . --config-file=pyproject.toml -``` - -Also run any additional checks that the repo exposes, such as: - -- service startup commands for Flask, FastAPI, Streamlit, or worker processes -- notebook kernel or import smoke checks -- devcontainer smoke validation when Docker/devcontainer tooling is available - -If a service exists, start it long enough to confirm it boots successfully. If -the environment prevents that, record the exact blocker. - -## Step 8. Report - -Finish with the structure in -[audit-report-template](../assets/audit-report-template.md). - -The report should include: - -- Scope and approved exceptions -- Findings before remediation -- Changes applied -- Re-audit result -- Validation commands run and results -- Remaining blockers or approved deviations - -## Decision Rules - -Use these rules when the target repo does not match the template cleanly. - -- Missing baseline feature: add it unless the user approved an exception. -- Different but equivalent implementation: keep it if it meets the same intent, - then document the equivalence. -- Docs contradict automation: trust the executable config first. -- Target repo contains stale template placeholders: replace them with target repo - values after copying the shipped base file rather than recreating the config. -- Validation unavailable due to environment: report it explicitly, with the - exact command that could not be run. diff --git a/.copilot/skills/audit-python-template-compliance/references/template-baseline.md b/.copilot/skills/audit-python-template-compliance/references/template-baseline.md deleted file mode 100644 index 7e88f219..00000000 --- a/.copilot/skills/audit-python-template-compliance/references/template-baseline.md +++ /dev/null @@ -1,200 +0,0 @@ -# TECHLETES Python Template Baseline - -Use this reference to decide what the target repository should look like when -it is aligned with the TECHLETES Python basic template. - -## Source-of-Truth Order - -When template sources disagree, prefer them in this order: - -1. Executable config and automation - `pyproject.toml`, `.pre-commit-config.yaml`, `.github/workflows/`, - `.devcontainer/`, `scripts/`, especially `scripts/hooks/` -2. Contributor-facing contract docs - `README.md`, `CONTRIBUTING.md`, `docs/` -3. Example code and tests - -Do not copy stale prose over working automation. Align the docs to the actual -behavior unless the automation is clearly outdated and should be changed. - -When you need exact config values for tool configuration, use -`template-config-settings.md` and the shipped asset files as the base, then -apply only the allowed target-specific substitutions. - -## Baseline Areas - -### 1. Platform and Dependency Management - -- Python baseline is `>=3.12,<4.0`. -- Dependency management is `uv`-first. -- Dependencies are declared in `pyproject.toml`. -- Locked dependency state is committed in `uv.lock`. -- Version constraints use bounded ranges such as `>=X.Y.Z,=X.Y.Z, dict[str, int]: + return {item: len(item) for item in items} + ``` + +### Rule 4: Commit and Virtual Environment +- **Always activate the virtual environment before committing** + - Linux/macOS: `source .venv/bin/activate` + - Windows: `.venv\Scripts\Activate.ps1` + - Or use direnv: `direnv allow` (auto-activates on `cd`) +- Pre-commit hooks require packages from the venv to run +- If hooks fail, fix issues and recommitโ€”this is normal and protects the repo +- Skip with `git commit --no-verify` only in emergencies (not recommended) + +--- + +## Quick Start + +### Initial Setup (First Time) +```bash +git clone https://github.com/TECHLETES/python_basic_template.git +cd python_basic_template +./scripts/setup.sh # Linux host setup +# On Windows or macOS, prefer the devcontainer workflow from docs/quickstart.md +``` + +### Activate Virtual Environment (Every Session) +```bash +source .venv/bin/activate # Linux/macOS +.venv\Scripts\Activate.ps1 # Windows +direnv allow # If using direnv +``` + +### Daily Commands + +| Task | Command | +|------|---------| +| **Install/update deps** | `uv sync` | +| **Add new package** | Edit `pyproject.toml`, then `uv lock && uv sync` | +| **Run tests** | `uv run pytest` | +| **Run tests with coverage** | `uv run pytest --cov` | +| **Format code** | `uv run black .` | +| **Lint and fix** | `uv run ruff check . --fix` | +| **Type check** | `uv run mypy . --config-file=pyproject.toml` | +| **Pre-commit all files** | `uv run pre-commit run --all-files` | +| **Load secret** | `from utils.secrets import get_secret; api_key = get_secret("op://...")` | + +--- + +## Project Structure & Key Files + +``` +utils/ # Production code (shared utilities) +โ”œโ”€โ”€ __init__.py # Enables runtime type checking +โ”œโ”€โ”€ secrets.py # 1Password CLI wrapper (get_secret) +โ””โ”€โ”€ [other modules] + +example/ # Example code & notebooks +โ”œโ”€โ”€ using_secrets.py # Demo of secret loading +โ””โ”€โ”€ using_secrets.ipynb # Jupyter with auto-stripped outputs + +tests/ +โ””โ”€โ”€ utils/ # Test structure mirrors production + โ””โ”€โ”€ test_*.py # Unit tests (prefix: test_) + +scripts/ +โ”œโ”€โ”€ setup.sh # Linux host environment initialization +โ”œโ”€โ”€ install-copilot-agents.sh # Installs repo-shared Copilot agents and skills +โ”œโ”€โ”€ new-branch.sh # Git workflow helper +โ””โ”€โ”€ hooks/ # Pre-commit hook scripts + โ”œโ”€โ”€ nbstripout-autoadd.sh # Auto-strip notebook outputs + โ””โ”€โ”€ run-pip-audit.sh # pip-audit wrapper used in hooks and CI + +.github/ +โ”œโ”€โ”€ workflows/ci.yml # GitHub Actions: pre-commit, tests, coverage, devcontainer smoke +โ”œโ”€โ”€ pull_request_template.md # PR checklist (includes security review) +โ””โ”€โ”€ dependabot.yml # Automated dependency updates + +docs/ +โ”œโ”€โ”€ 0_setup.md # Setup walkthrough +โ”œโ”€โ”€ 1_usage.md # Daily development guide +โ”œโ”€โ”€ 2_secret_management.md # 1Password integration +โ”œโ”€โ”€ 3_dependency_management.md # Using uv +โ”œโ”€โ”€ 4_code_quality.md # Typing & linting tools +โ”œโ”€โ”€ 5_pre_commit_hooks.md # Hook explanations +โ””โ”€โ”€ 6_pre_commit_troubleshooting.md + +pyproject.toml # Single source of truth (metadata, deps, tool configs) +uv.lock # Locked dependency versions (auto-generated, commit it) +.pre-commit-config.yaml # 16+ pre-commit hooks configuration +``` + +--- + +## Key Conventions + +### Code Organization +- **Production code:** Place in `utils/` or `example/` packages +- **Tests:** Mirror production structureโ€”test `utils/foo.py` in `tests/utils/test_foo.py` +- **Test naming:** Functions `test_*()`, classes `Test*` +- **Small functions:** Prefer composable, testable units over large monoliths + +### Type Hints +- Required on all functions/methods (enforced by mypy config: `disallow_untyped_defs = true`) +- Use modern syntax: `list[str]`, `dict[str, int]` (Python 3.9+) +- Return types always specified: `def foo(...) -> ReturnType:` +- Example valid function: + ```python + def validate_secret_path(path: str) -> bool: + return path.startswith("op://") + ``` + +### Code Style +- **Line length:** 88 characters (Black formatter) +- **Imports:** Sorted by Ruff import-sorting rules, auto-fixed by pre-commit +- **Modernization:** pyupgrade auto-converts old syntax (Python 3.12+) +- **Docstrings:** Checked by pydocstyle (Google style recommended) + +### Dependencies +- **Declare in:** `pyproject.toml` only +- **Syntax:** `package>=MAJOR.MINOR.Z, str +# - Use Optional[T] for nullable values: Optional[str] +# - Use Union[T1, T2] for multiple types: Union[int, str] +# - Import from typing: from typing import Optional, Union, Any +``` + +### Manage CVE Exclusions for pip-audit +```bash +# Edit pyproject.toml [tool.pip-audit] section to add/remove exclusions +# Both pre-commit and CI will automatically use the updated list + +[tool.pip-audit] +ignore = [ + "CVE-2026-1703", # Brief explanation of why this CVE is excluded + "CVE-2024-XXXXX", # Add new exclusions here +] + +# Run pip-audit locally to verify +uv run bash scripts/hooks/run-pip-audit.sh --progress-spinner off --desc + +# Script automatically reads from pyproject.toml and applies exclusions +``` + +### Debug Pre-Commit Hook Failures +```bash +# Run a specific hook manually +uv run pre-commit run black --all-files +uv run pre-commit run mypy --all-files +uv run pre-commit run bandit --all-files + +# Run all hooks +uv run pre-commit run --all-files + +# See logs if a Python tool failed +# Edit the file to fix the issue, then commit again +``` + +### Load a Secret in Code +```python +from utils.secrets import get_secret + +# Load secret and use directly +api_token = get_secret("op://Engineering/GitHub/token") + +# Or save to environment variable for subprocess +get_secret("op://Engineering/Database/password", "DB_PASSWORD") +# Now can access: os.environ["DB_PASSWORD"] +``` + +### Create a Test +```python +# File: tests/utils/test_my_module.py +from utils.my_module import my_function + +def test_my_function_returns_correct_value() -> None: + result = my_function("input") + assert result == "expected_output" + +def test_my_function_handles_edge_case() -> None: + result = my_function("") + assert result is not None +``` + +### View Test Coverage +```bash +pytest --cov=utils --cov=example --cov-report=html +# Opens: htmlcov/index.html +``` + +--- + +## Common Issues & Solutions + +### โŒ Error: "pytest: command not found" +**Cause:** Virtual environment not activated +**Fix:** +```bash +source .venv/bin/activate # Linux/macOS +.venv\Scripts\Activate.ps1 # Windows +``` + +### โŒ Error: "Pre-commit hook failed" on commit +**Cause:** Code doesn't pass quality checks +**Fix:** +```bash +pre-commit run --all-files # See exactly which checks failed +# Fix the issues (most auto-fix, some require manual edits) +git add . +git commit # Retry +``` + +### โŒ Error: "mypy: disallow_untyped_defs" +**Cause:** Function missing type hints +**Fix:** Add type hints to parameters and return value +```python +# โŒ Before +def process_items(items): + return len(items) + +# โœ… After +def process_items(items: list[str]) -> int: + return len(items) +``` + +### โŒ Error: "Secret detected in file" +**Cause:** detect-secrets hook blocked a secret +**Fix:** +1. Remove the secret from the file +2. Use `get_secret()` instead: + ```python + from utils.secrets import get_secret + api_key = get_secret("op://Vault/Item/field") + ``` +3. Commit again + +### โŒ Error: "op: command not found" +**Cause:** 1Password CLI not installed +**Fix:** See `CONTRIBUTING.md` for 1Password CLI setup +```bash +# Verify installation +op --version +op account list # Authenticate +``` + +### โš ๏ธ Issue: Notebook outputs committed to git +**Cause:** nbstripout hook didn't run (venv likely inactive) +**Fix:** +```bash +# Activate venv +source .venv/bin/activate + +# Strip outputs manually +jupyter nbconvert --ClearingPreprocessor.enabled=True example/using_secrets.ipynb + +# Commit again +``` + +### โš ๏ธ Issue: "Edited uv.lock" shows up in diffs +**Cause:** Manually edited uv.lock (should never happen) +**Fix:** +```bash +# Regenerate correct lock file +uv lock + +# Commit the regenerated version +``` + +--- + +## Pre-Commit Hook Troubleshooting + +See `docs/6_pre_commit_troubleshooting.md` for detailed troubleshooting of individual hooks. + +**Quick reference:** +- **Most hooks auto-fix:** Run `pre-commit run --all-files` to auto-fix, then recommit +- **Some hooks require manual fixes:** mypy, bandit, pydocstyle, detect-secrets +- **Skip all hooks (last resort only):** `git commit --no-verify` +- **Skip config in direnv:** Edit `.envrc` โ†’ run `direnv allow` + +--- + +## File Preferences for AI Coding + +### When Writing Code +1. **Type hints:** Always include themโ€”required for mypy +2. **Docstrings:** Use Google style format; checked by pydocstyle +3. **Functions:** Keep them small and testable (test each function) +4. **Imports:** Use absolute imports, not relative (`from utils ...` not `from ..utils`) +5. **Line length:** Aim for โ‰ค88 characters (Black will format longer lines) + +### Example: Well-Structured Function +```python +"""Module for processing user data.""" + +from typing import Optional +from utils.logging import get_logger + +logger = get_logger(__name__) + + +def validate_email(email: str) -> bool: + """Check if email format is valid. + + Args: + email: Email address to validate. + + Returns: + True if email is valid, False otherwise. + """ + import re + pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" + is_valid = bool(re.match(pattern, email)) + logger.debug(f"Validated email: {is_valid}") + return is_valid +``` + +--- + +## References + +- **README:** Overview and essential rules +- **docs/0_setup.md:** Step-by-step setup +- **docs/1_usage.md:** Daily development guide +- **docs/2_secret_management.md:** 1Password integration +- **docs/3_dependency_management.md:** Using `uv` +- **docs/4_code_quality.md:** Type hints & linting +- **docs/5_pre_commit_hooks.md:** Hook details +- **docs/6_pre_commit_troubleshooting.md:** Troubleshooting +- **CONTRIBUTING.md:** Contribution guidelines +- **pyproject.toml:** Tool configurations (pytest, mypy, ruff, black, etc.) + +--- + +## For AI Assistants (Copilot/Claude) + +When working on code in this repository: + +โœ… **DO:** +- Enforce type hints on all functions (required, not optional) +- Follow the 88-character line limit +- Add comprehensive docstrings (Google style) +- Write tests in `tests/utils/test_*.py` mirroring production structure +- Use `from utils.secrets import get_secret()` for any secrets +- Suggest dependency additions to `pyproject.toml`, not direct pip installs +- Check pre-commit hook configurations for lint/format requirements + +โŒ **DON'T:** +- Write untyped functions (will fail mypy) +- Put secrets or API keys in code +- Use relative imports (`from ..module` instead of `from utils.module`) +- Suggest running `pip install` directly +- Make manual edits to `uv.lock` +- Assume pre-commit hooks should be skipped +- Create imports of unlisted dependencies (always add to `pyproject.toml` first) + +--- + +*Last updated: 2026-03-03* +*For questions or updates, see CONTRIBUTING.md* From b65fdef38967fb6679d67775e0fad1cf83d26cf1 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 3 Jun 2026 10:07:29 +0000 Subject: [PATCH 154/199] fix: streamline post-create script for virtual environment setup --- .devcontainer/post-create.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 5a542f19..0a46d3f3 100644 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -17,9 +17,7 @@ echo "Bootstrapping the container workspace with uv..." uv --version python --version -uv venv .venv --allow-existing -source .venv/bin/activate -uv sync --frozen --all-groups --active +uv sync --frozen --reinstall uv tool install graphifyy if [[ -d .git ]]; then From 521eca77845842f501df62b1e8c9d9820a8b356b Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Wed, 3 Jun 2026 10:18:25 +0000 Subject: [PATCH 155/199] fix: update post-create script for improved virtual environment setup --- .devcontainer/devcontainer.json | 1 + .devcontainer/post-create.sh | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d2d0675d..9434c530 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -71,5 +71,6 @@ "postCreateCommand": "bash .devcontainer/post-create.sh", "remoteUser": "vscode", "updateRemoteUserUID": true, + "waitFor": "postCreateCommand", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}" } diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 0a46d3f3..5a542f19 100644 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -17,7 +17,9 @@ echo "Bootstrapping the container workspace with uv..." uv --version python --version -uv sync --frozen --reinstall +uv venv .venv --allow-existing +source .venv/bin/activate +uv sync --frozen --all-groups --active uv tool install graphifyy if [[ -d .git ]]; then From 36b6abb70ce7aa1d0c60daa1086bd2939bd3407f Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 6 Jul 2026 17:26:14 +0200 Subject: [PATCH 156/199] Refactor code structure for improved readability and maintainability --- .devcontainer/devcontainer-lock.json | 10 +++ .devcontainer/devcontainer.json | 38 ++++++--- scripts/hooks/run-pip-audit.sh | 2 +- uv.lock | 120 ++++++++++++++------------- 4 files changed, 103 insertions(+), 67 deletions(-) diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index 717056d0..56eb8164 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -4,6 +4,16 @@ "integrity": "sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e", "resolved": "ghcr.io/devcontainers/features/docker-outside-of-docker@sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e", "version": "1.10.0" + }, + "ghcr.io/devcontainers/features/git:1": { + "integrity": "sha256:a3e43ff91b9f5f6bd2c14bd510d43e5e698f1266dc41027ba4e04e7e45be607a", + "resolved": "ghcr.io/devcontainers/features/git@sha256:a3e43ff91b9f5f6bd2c14bd510d43e5e698f1266dc41027ba4e04e7e45be607a", + "version": "1.3.7" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", + "version": "1.1.0" } } } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9434c530..58d2e5f6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,10 +5,11 @@ }, "containerEnv": { "PRE_COMMIT_HOME": "/home/vscode/.cache/pre-commit", - "PYTHONPATH": "${workspaceFolder}", + "PYTHONPATH": "/workspaces/${localWorkspaceFolderBasename}", + "TZ": "Europe/Amsterdam", "UV_CACHE_DIR": "/home/vscode/.cache/uv", "UV_LINK_MODE": "copy", - "UV_PROJECT_ENVIRONMENT": "${workspaceFolder}/.venv", + "UV_PROJECT_ENVIRONMENT": "/workspaces/${localWorkspaceFolderBasename}/.venv", "UV_PYTHON_DOWNLOADS": "never" }, "customizations": { @@ -22,7 +23,6 @@ "charliermarsh.ruff", "ms-toolsai.jupyter", "astral-sh.ty", - "charliermarsh.ruff", "ms-python.autopep8", "ms-python.black-formatter", "ms-python.debugpy", @@ -30,8 +30,8 @@ "docker.docker", "dbaeumer.vscode-eslint", "tamasfe.even-better-toml", - "cweijan.vscode-excel", - "GitHub.copilot-chat", + "cweijan.vscode-office", + "openai.chatgpt", "github.vscode-github-actions", "GitHub.vscode-pull-request-github", "ms-azuretools.vscode-azure-github-copilot", @@ -40,10 +40,13 @@ "mtxr.sqltools-driver-pg", "mtxr.sqltools-driver-sqlite", "ms-ossdata.vscode-pgsql", + "formulahendry.docker-explorer", "biomejs.biome", "bradlc.vscode-tailwindcss", "ms-playwright.playwright", - "me-dutour-mathieu.vscode-github-actions" + "me-dutour-mathieu.vscode-github-actions", + "matthewpi.caddyfile-support", + "charliermarsh.ruff" ], "settings": { "jupyter.jupyterServerType": "local", @@ -54,21 +57,36 @@ ], "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, - "ruff.enable": true + "ruff.enable": true, + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.env.linux": { + "PYTHONPATH": "${workspaceFolder}", + "UV_PROJECT_ENVIRONMENT": "${workspaceFolder}/.venv" + } } } }, "features": { - "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { + "enableNonRootDocker": "true", + "moby": "false", + "version": "latest" + }, + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} }, "mounts": [ - "source=python-template-uv-cache,target=/home/vscode/.cache/uv,type=volume", - "source=python-template-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume", + "source=fst-uv-cache,target=/home/vscode/.cache/uv,type=volume", + "source=fst-pre-commit-cache,target=/home/vscode/.cache/pre-commit,type=volume", "source=${localEnv:HOME}/.codex,target=/home/vscode/.codex,type=bind,consistency=cached" ], "name": "python-basic-template", "overrideCommand": true, "postCreateCommand": "bash .devcontainer/post-create.sh", + "remoteEnv": { + "CAVEMAN_DEFAULT_MODE": "full", + "CODEX_HOME": "/home/vscode/.codex" + }, "remoteUser": "vscode", "updateRemoteUserUID": true, "waitFor": "postCreateCommand", diff --git a/scripts/hooks/run-pip-audit.sh b/scripts/hooks/run-pip-audit.sh index 0202b037..750c0795 100755 --- a/scripts/hooks/run-pip-audit.sh +++ b/scripts/hooks/run-pip-audit.sh @@ -24,4 +24,4 @@ EOF ) # Run pip-audit with the exclusions from pyproject.toml -pip-audit $IGNORE_ARGS "$@" +uv run pip-audit $IGNORE_ARGS "$@" diff --git a/uv.lock b/uv.lock index d83411d7..3eb58e20 100644 --- a/uv.lock +++ b/uv.lock @@ -874,46 +874,54 @@ wheels = [ [[package]] name = "msgpack" -version = "1.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, - { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, - { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, - { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, - { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, - { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, - { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, - { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, - { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, - { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, - { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, - { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, - { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, - { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, - { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, - { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, - { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, ] [[package]] @@ -1122,11 +1130,11 @@ wheels = [ [[package]] name = "pip" -version = "26.0.1" +version = "26.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, ] [[package]] @@ -1899,19 +1907,19 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, - { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, - { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, - { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, - { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, - { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, - { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +version = "6.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, + { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, + { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, ] [[package]] From 88286f67f918762c86df7caf8aca8ec24a56667d Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 6 Jul 2026 17:27:36 +0200 Subject: [PATCH 157/199] Implement code changes to enhance functionality and improve performance --- uv.lock | 1267 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 645 insertions(+), 622 deletions(-) diff --git a/uv.lock b/uv.lock index 3eb58e20..e03547d0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.12, <4.0" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] [[package]] name = "appnope" @@ -22,11 +26,11 @@ wheels = [ [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] @@ -55,7 +59,7 @@ wheels = [ [[package]] name = "black" -version = "26.3.1" +version = "26.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -65,24 +69,24 @@ dependencies = [ { name = "platformdirs" }, { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, - { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, - { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, - { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, - { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, - { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, - { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, - { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, - { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, + { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, ] [[package]] @@ -123,11 +127,11 @@ filecache = [ [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] @@ -198,80 +202,80 @@ wheels = [ [[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/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" }, +version = "3.4.8" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/c2/39de60ef5687662f467bed3d1e6944c67a4f0d057141d0404002b8f405ae/charset_normalizer-3.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:faac37c4904598daa00cb4c9b32f3b4cc814fb5f145d7a531ceb4a70f2114132", size = 319040, upload-time = "2026-07-06T15:26:15.854Z" }, + { url = "https://files.pythonhosted.org/packages/a7/57/a9474c3aeaa337c8a330c0dc5df266527d56da3b189c029529f6b08af2a4/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f191c19a32dc6cec0fb8079789d786254653a9ce906fcab04ccd2eed07bba233", size = 215541, upload-time = "2026-07-06T15:26:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/13/a9/be1ff7e81f6e086dced2a7a7a28b789be351d9796084ccaf6136a4ffafb3/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05811b76943d477bb90822dedb5c4565cef70148847a59d574e2b35043aeb563", size = 236913, upload-time = "2026-07-06T15:26:18.482Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/d8c5eae93da26d463f9ebe46a4937ca44434dc2937a565b92437befb3d94/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3868a3e4ec1e40b419e060d063f93eac6f046fa21426c4816421223ae7dc8ab8", size = 232815, upload-time = "2026-07-06T15:26:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/98e301ca944bcca5e6bc312406b579c8a6d81546c1b494afb3a9478495d6/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25f93d194eb6264c64416cabff46a91f6d99b97e7525a1b4f35c77a99e75cc68", size = 223995, upload-time = "2026-07-06T15:26:20.931Z" }, + { url = "https://files.pythonhosted.org/packages/fa/df/f5222366b76dcb31453a9bd922610c893540d0e729fd390439b0d3e972ee/charset_normalizer-3.4.8-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:ee6a62492f18d432cca031fabd158f400a8c25bf7b9458f50953393a2a23d97a", size = 208522, upload-time = "2026-07-06T15:26:22.214Z" }, + { url = "https://files.pythonhosted.org/packages/74/52/293220d59d8ddfb8aa56836b33bd6df58e70795d8a102a858c2984480f00/charset_normalizer-3.4.8-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c16cb4fc35e4b064f5ee78d849f15a550ada1729c3372916672e38f1f01d1d4", size = 219660, upload-time = "2026-07-06T15:26:23.493Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2c/81a298e66f3d01e61bfc6f7064bbb553b067a9f1d979e5962bf00733069b/charset_normalizer-3.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fbd0edb0426ab28e70fac9d1d4ef549eb5a64a2521f0428c441d75e4387e6c2", size = 218230, upload-time = "2026-07-06T15:26:24.629Z" }, + { url = "https://files.pythonhosted.org/packages/0d/45/f1dd2328cbc3340705f82072c09bd4c68d6e079191cde05810c1eac77eee/charset_normalizer-3.4.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccb9052771216170015f810b88065fd9e13b1e0b391f92abb9b47e0919a42aad", size = 210006, upload-time = "2026-07-06T15:26:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/28697000620e117eb413424caaf60b6f98ddb1b09b2c11f7c0038d9936a7/charset_normalizer-3.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3809ba5d3cd02aca0894597f2669a825bdfe2229061515c128b0f4e5533b4ab5", size = 225771, upload-time = "2026-07-06T15:26:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/12/a7/d5844e315f5f35e7938c415f07a1df144eed1cf993f1b43cc16c980c5b46/charset_normalizer-3.4.8-cp312-cp312-win32.whl", hash = "sha256:de63c31666a049f653ada24e800192e3c019e96bc7d70fb449a000bccf26a36f", size = 150922, upload-time = "2026-07-06T15:26:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/9b/82/eb8b72f184b1e4986dd9daec15d7f6d9285a6728d2b07b7f04656829f473/charset_normalizer-3.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:14a4bbe066f3fb05c6ba70e9cf9d34614b57a2fd70ea8c27cc30f34155e16a58", size = 162294, upload-time = "2026-07-06T15:26:29.745Z" }, + { url = "https://files.pythonhosted.org/packages/09/5a/ab810134aa41034a08ffe94c058102016e6ad9bce62f3cdba547b4723385/charset_normalizer-3.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:2b5b0c0dca0a02c3f816f89abf18af3d20416dedbc3d3aa5f3981045f88ae7b0", size = 152409, upload-time = "2026-07-06T15:26:31.034Z" }, + { url = "https://files.pythonhosted.org/packages/11/49/fe5a8572a70cd9cba79f80af9388ac8c5c914ed4459b956f940244e499a5/charset_normalizer-3.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:057f8609f7341618c98e5aa9a6109fa116acff2a658497d47ab3325b5e8f2b08", size = 317424, upload-time = "2026-07-06T15:26:32.23Z" }, + { url = "https://files.pythonhosted.org/packages/e5/59/d71c96616b6825425a876f79f38fa440db30b32cc1166179a839f6259150/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c591f9a82adc5b89a039b90df74e43de2b9177fb46771172bed7b80722a70db0", size = 214723, upload-time = "2026-07-06T15:26:33.635Z" }, + { url = "https://files.pythonhosted.org/packages/c6/35/dc9eeb297f19b7b6ada39709ccb74937e6c51f0947958ae601a977cedd5d/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b56f449132d9adefe55b87635d05177a914ed5d070438a74725e1d77a280002", size = 236200, upload-time = "2026-07-06T15:26:34.99Z" }, + { url = "https://files.pythonhosted.org/packages/0a/37/6775fe852b4acad8bf7e0575fbe8aa9f41b546e33251acbded3c04a6b0d9/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1004c3b5831a301dadfb9e916f38e78e2ff3e08db24a1ad7c354db8ee3dea9c3", size = 231740, upload-time = "2026-07-06T15:26:36.498Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/1bcc3f5f3bac81532384adcfcdd9362c7f46a188a19deacc1ddaf7bdaa00/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60883e22821d17c9e5b4f3ca1ef8074f766e3db28791f851b665929c515635c0", size = 222888, upload-time = "2026-07-06T15:26:38.161Z" }, + { url = "https://files.pythonhosted.org/packages/47/81/9f3993ca62ef090c58059da641e49e3129e74700a6a3beb58436cdb8d4b9/charset_normalizer-3.4.8-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:21e4dbb942c8a6342e2685f232dd2a7bc73465697bd26ead4f118271d28be383", size = 207649, upload-time = "2026-07-06T15:26:39.628Z" }, + { url = "https://files.pythonhosted.org/packages/11/f4/679f636bcbdc2d53d06b1f4039be310450dca95a9f76bbf22f09985556e8/charset_normalizer-3.4.8-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84bcae14c65e645ca66b661339183d32b8c846a17c96e3e81ab3d346e1c498d4", size = 218908, upload-time = "2026-07-06T15:26:40.911Z" }, + { url = "https://files.pythonhosted.org/packages/7e/44/96e8c81867ba8a45ff893c8e7474c2d6b9633f7aa663da7901d040214d3e/charset_normalizer-3.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4e4d44b8287aa13a25e16e29393d494b0643b24894f7c8266c6f6788dd36337", size = 217096, upload-time = "2026-07-06T15:26:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/a5/bf/4d53f04f29bdb22601701f4f9f4d038edfb27976c296fcb7400c02736a6e/charset_normalizer-3.4.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f68545d1b267dbfafd5d253b6d1cb161562c4e61ab25b5c4cdb7d9e5923e441e", size = 209355, upload-time = "2026-07-06T15:26:43.557Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/92b3f630798d777fa880ad289a3f9f2fc663e4b4beb24783c53318820254/charset_normalizer-3.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1936e48214adea74922a20c8ab41b1393ae27cc9e329eb1f0b937d3416824f36", size = 224732, upload-time = "2026-07-06T15:26:44.892Z" }, + { url = "https://files.pythonhosted.org/packages/bd/85/eafa0a3c7bb6fe9f02f4c7901f02071933cac85ee634197e17280818c6de/charset_normalizer-3.4.8-cp313-cp313-win32.whl", hash = "sha256:1f8e3521860187d597f3867d8466da225b9179ea2833bb26de1bb026144d07c3", size = 150358, upload-time = "2026-07-06T15:26:46.153Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8c/879fafff7b47bb1166d289f2d2472cb31b9922f9f4ca1f392edf85ec16be/charset_normalizer-3.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:8b654b6f52a0a9a6be38e88f3e1dc68f1093ebeb2abbadafc7c82da0786a34be", size = 161685, upload-time = "2026-07-06T15:26:47.423Z" }, + { url = "https://files.pythonhosted.org/packages/c3/46/b57c7e778a7b578f28d35fd38544687d4f8d9c019585eebc5ad936073fad/charset_normalizer-3.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:d2d5a250ee26e29468b7607d97479221b069fa8aaf6f929ac84ec0e962e15154", size = 152333, upload-time = "2026-07-06T15:26:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/0a8540b8cd494951cca1428606373942803f5ffcec40fe798f819c5a8adb/charset_normalizer-3.4.8-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:77e993ecf65f21ab1f82266ff5e84a7de2c879e7d9b8bc006009df83f22a1d5e", size = 316993, upload-time = "2026-07-06T15:26:49.962Z" }, + { url = "https://files.pythonhosted.org/packages/0e/99/a0868f0a1f0a045fd374d1f2cf7042d8ad5d7fb4dd1f4ac7365e319f7e32/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:524939917f17f6de502dfda30b472550965740d7f126659d4c4f8dd1569cce22", size = 215638, upload-time = "2026-07-06T15:26:51.338Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/43c4d09a09b5557cc5fe1d87c9d96f86a3942aec0517d2b5408cef87ca75/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a4508989ba8e2ce43ef989453d18188b261546e8188cbdd4ef451fb9e4c3b467", size = 236456, upload-time = "2026-07-06T15:26:52.531Z" }, + { url = "https://files.pythonhosted.org/packages/e2/67/492ca98b3ab785b736b5da10c1bc233e1c8fec6c0cdb29b482c38bfc52a2/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e44127f7d11eee4548ad2cdf1f4e1b6eaaddd5cb92d15ad65f6ecc9bcf403ab", size = 232253, upload-time = "2026-07-06T15:26:53.838Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/1e6eff58c14f1aace1e26d80defbeaea2d35e075dbe4b611111ee4b47fa8/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb90317359f7e67bb6df615999a95e0980877468e617ddce8b6c2f8e7fe60d95", size = 222886, upload-time = "2026-07-06T15:26:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/40/7a/90056a5326b0c4b9a3f924d337729c344c11542e5bc7191e50410db61587/charset_normalizer-3.4.8-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:35d9e7a9c960520ae89d1f4e305d1c047a74dea2e0f73a0e84f879356c2e8776", size = 206482, upload-time = "2026-07-06T15:26:56.306Z" }, + { url = "https://files.pythonhosted.org/packages/18/ff/94761d31a33878dbb5008ddbd918615061fcf5c0a612aa3075450e60f628/charset_normalizer-3.4.8-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:92e322b054c7ff886f78feab7360736bb45de2e18cf4a0ee84e8fc5a08d53a19", size = 218929, upload-time = "2026-07-06T15:26:57.422Z" }, + { url = "https://files.pythonhosted.org/packages/2b/dc/00b9675acd7c4b926b9102ee3f0d1a570ce943901be73b87485001393fe1/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c0086d97094363556206dc3bcf43f7edcfc043ea7a568a46f45efea74858bd1", size = 218069, upload-time = "2026-07-06T15:26:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/04/11/94ada5a0482ee4bf688d04be4c7d6fd945d37370d04a95671040dfe2b416/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0752c849b51198267df2aba013c4de3a2955bd014a4fd70828809946c1acbc0c", size = 207146, upload-time = "2026-07-06T15:27:00.058Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/246bd36762207ab4752cd436b64e5d81a1668b15ddea7b5b2d0e8545e727/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2a4707e09eca11e81ece4fced600c5a0a801f568b962244f6f517bc274745fc9", size = 224896, upload-time = "2026-07-06T15:27:01.599Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f7/3510622d1fbe13b0ebf827c475e40a27e2be427140d792878b63ab6425cc/charset_normalizer-3.4.8-cp314-cp314-win32.whl", hash = "sha256:8ea67f427c073ae3da0923aa55f3715131fa613a61a7f2f8d762bde75eaf00ae", size = 150851, upload-time = "2026-07-06T15:27:02.964Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/9ce65dd21672b55cf800cca5f4433afa1586fda1d78731067ec9ec544c62/charset_normalizer-3.4.8-cp314-cp314-win_amd64.whl", hash = "sha256:ff71018850863362e5c7533769d0a9f77715c31af1502d523630ce822922f5c9", size = 162549, upload-time = "2026-07-06T15:27:04.249Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/9a5967eed666a88f31a0866884606d9ec3c2cd6091e2ccd7e0b4c4176c35/charset_normalizer-3.4.8-cp314-cp314-win_arm64.whl", hash = "sha256:44464e66f4da2f21dea7145c7693f9f60717ca4794a954dea5bf8c2c932678bd", size = 153079, upload-time = "2026-07-06T15:27:05.608Z" }, + { url = "https://files.pythonhosted.org/packages/02/4f/aa44cc81d8987f105352c74c0bf919007f8b80e9880d28bcf0393c1a816e/charset_normalizer-3.4.8-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:50a0c2e58ad2c203adb616fef28941b7e13716adbc25e0dfaeec29f5afe6382f", size = 338586, upload-time = "2026-07-06T15:27:06.86Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2b/b0392e2b235c08ff0623d905c2ee8ac820620544043c1ce92ce0b3d64c55/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1e589fdb95c76f08288bbb346230cdd8994db74903db6637b380f7b5fc9336", size = 222764, upload-time = "2026-07-06T15:27:08.23Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7d466879190731f5559662c22232646f2ae2dace2323c3e5aefcf78d458a/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3d7c887444c5a7ef0d68d358d81e758a850bc626f8e639e2ca5667153272b20", size = 241331, upload-time = "2026-07-06T15:27:09.512Z" }, + { url = "https://files.pythonhosted.org/packages/70/17/8b89e797137aa28c8fb0bafbafc243246a7afe21620a13b00e37624ece1d/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:65c389b96c0cfff3a3f0458fa1c7ce554a30e23101a88a49f03997afce6a929f", size = 239323, upload-time = "2026-07-06T15:27:10.86Z" }, + { url = "https://files.pythonhosted.org/packages/d7/98/1c1940730ed22d50983be4e243c722c89d5136d6f073bd840d1128bfddcb/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:593403fc47dcdf55e2987b2e3cc2e064127e2b908929f1f18b2e4a4652cbd780", size = 229964, upload-time = "2026-07-06T15:27:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/53/2d/bb8e81b7ff603d3f77e9a8a5d1ad34fcabbf3c54d300c29d99fba581fa23/charset_normalizer-3.4.8-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:606088e9fa2b7469ab9c42d4da8e05a415622a07714edd2fcd8fed48dda4c853", size = 212405, upload-time = "2026-07-06T15:27:13.447Z" }, + { url = "https://files.pythonhosted.org/packages/ce/1f/e52a3a53b13da591bb8f21d29e63877268eadf20686b7762351d4b89062c/charset_normalizer-3.4.8-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0317406326fed512f42a1632ad91a96228a7616c06547666a6dd79967f1bd6ca", size = 226918, upload-time = "2026-07-06T15:27:14.89Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f9/32996d79c57189af9722fe618f46d8a86b7be035ca98887b8d0c3821f141/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b67d50ee47e5c57a0064a9cb575b963a7125819dfd1fd094d44d378fff94659b", size = 225113, upload-time = "2026-07-06T15:27:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d2/9248c18e695696513774523a794cfb8b677521ce9ad7554d301cb10a9b20/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79e402b869f270140afa5e2b0e2ac100585358d812fe3dd093d424f7a72964e0", size = 214966, upload-time = "2026-07-06T15:27:17.418Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9d/4b19432d406179a40f924691906ee5b15ac664b408971c973295192444ea/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2970b9f7ab69ec3a0423ec6b6ac718e79fbf4a282c0bc103ef88c1ef50dfa15a", size = 231699, upload-time = "2026-07-06T15:27:19.131Z" }, + { url = "https://files.pythonhosted.org/packages/be/41/bdbdf71e8c3ccff10ef3cc2bb9467a7fdb3dc94b9a406d1a3c44afd39632/charset_normalizer-3.4.8-cp314-cp314t-win32.whl", hash = "sha256:458c2972a78043b7261c9726670029f15f722e70669bcbe961153a01968f589f", size = 155333, upload-time = "2026-07-06T15:27:20.681Z" }, ] [[package]] name = "click" -version = "8.3.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] name = "codespell" -version = "2.4.1" +version = "2.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" }, + { url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" }, ] [[package]] @@ -294,91 +298,76 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, - { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, - { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, - { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, - { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, - { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, - { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, - { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, - { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, - { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, - { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, - { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, - { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, - { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, - { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, - { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, - { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, - { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, - { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, - { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, - { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, - { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, - { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, - { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, - { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, - { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, - { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, - { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, - { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, - { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, - { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, - { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, - { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, - { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +version = "7.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/74/fd4c0901137c4f8d81a76ada99e43c65163b4c94a02ece107a4ec0c6b615/coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d", size = 220838, upload-time = "2026-07-02T13:09:02.084Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2e/2347583467bd7f0402635101a916961915cc68fce652cd0db5f173ea04fc/coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe", size = 221197, upload-time = "2026-07-02T13:09:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/f0/17/99fa688541ae1d6e84543a0e544f83de0c944815b63e9e7b1ed411d15036/coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c", size = 252705, upload-time = "2026-07-02T13:09:05.059Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/6a95a5cd83b74839017ef9cf48d2d8c9ae60af919e17a3f336e6f9f1b7bd/coverage-7.15.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:20d9ccc4ebd0edc434d86dfd2a1dd2a8efa6b6b3073d0485a394fee86459ebb4", size = 255441, upload-time = "2026-07-02T13:09:06.559Z" }, + { url = "https://files.pythonhosted.org/packages/67/f2/406f6c57d600f68185942422c4c00f1a3255d60aee6e5fd961425cd9987e/coverage-7.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c8a976c365c8cb12f0cbd099508772ea41fb5fa80657a8506df0e11bd278c5", size = 256556, upload-time = "2026-07-02T13:09:08.197Z" }, + { url = "https://files.pythonhosted.org/packages/74/8e/d3fa48489c15ecdec1ba48fd61f68798555dddd2f6716f9ad42adeb1a2a9/coverage-7.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f948fd5ba1b9cbca91f0ae08b4c1ce2b139509149a435e2585d056d57d70bf01", size = 258815, upload-time = "2026-07-02T13:09:09.691Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/2d40ddd110462c6a2769677cf7f1c119a52b45f568978fc6c98e4cc0dd0f/coverage-7.15.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f58185f06edf6ad68ec9fb155d63ef650c82f3fbd7e1770e2867751fb13158f4", size = 253117, upload-time = "2026-07-02T13:09:11.212Z" }, + { url = "https://files.pythonhosted.org/packages/51/c0/310782f0d7c3cb2b5ac05ba8d205fe91f24a36f6bf3256098f1782181c38/coverage-7.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02adc79a920c73c647c5d117f55747df7f2de94571884758ce8bc58e04f0a796", size = 254475, upload-time = "2026-07-02T13:09:13.029Z" }, + { url = "https://files.pythonhosted.org/packages/86/f7/702da6c275f8ae6ade423d2877243122932c9b27f5403003b9ef8c927d12/coverage-7.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6eb7c300fbed667fd6e3588eba71c1904cdb06110ca6fdf908c26bdd88b8e382", size = 252619, upload-time = "2026-07-02T13:09:14.699Z" }, + { url = "https://files.pythonhosted.org/packages/fb/84/c5b15a7e5ecba4e56218d772d99fe80a63e63f8d11f12783723a6005ab45/coverage-7.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b5fb23fa2de9dce1f5c36c09066d8fcda16cd96e8e26686caa2d7cb9b567d65c", size = 256689, upload-time = "2026-07-02T13:09:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/95/2f/c8b07559b57701230c61b23a953858c052890c12ef568d81780c6c46e92e/coverage-7.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cec79341dbe6281484024979976d0c7f22beae08b4a254655decd25d42cbe766", size = 252189, upload-time = "2026-07-02T13:09:17.828Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/6d2f049dd3fd3dbfd60b62ba6b2162a04009e2c002ce70b24cf3878dec7a/coverage-7.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c664c5444b1d970b1b2a450e21fb19ee5c9cfdf151ded2dda37260031cca0da", size = 254059, upload-time = "2026-07-02T13:09:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/ce/92/b0287a2c42031d25c628f815f89a3cd9f8268ee78bb1252c9356cda1c689/coverage-7.15.0-cp312-cp312-win32.whl", hash = "sha256:5f764a3fa339bde6b3aa97657f5a6a3a9451e4a5b4ea98a2892c773a43525f77", size = 222893, upload-time = "2026-07-02T13:09:20.812Z" }, + { url = "https://files.pythonhosted.org/packages/a9/69/e34c481915fecb499b3146975061dac528752e37706edc1804f32c822469/coverage-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:52f9a4d2c4c56c8848bc2f524916698354b0211488b38c49ad9ae54f6cafbff6", size = 223429, upload-time = "2026-07-02T13:09:22.315Z" }, + { url = "https://files.pythonhosted.org/packages/fe/98/6e878f0b571d32684ef3f38d7c03db241ca5b82a5da8a5391596a8f209c4/coverage-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:31e5c3e70c85307ea35a12964e2e40f56ca2ee4b1c8c721ccf4609d17071080b", size = 222810, upload-time = "2026-07-02T13:09:23.812Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/145a3748098bcc86b631a85408d2c3dc5c104e0bd86d605468239b25b6c4/coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782", size = 220863, upload-time = "2026-07-02T13:09:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/4ed55708fed2c64b63c9bc5715daef670872202101938869b7fe5d5fbb8f/coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430", size = 221230, upload-time = "2026-07-02T13:09:26.897Z" }, + { url = "https://files.pythonhosted.org/packages/7b/19/3a80b97d3b2a5c77a01ae359c6bed20c13738fe3d9380f08616d4fec0281/coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5", size = 252227, upload-time = "2026-07-02T13:09:28.543Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/b70062750686bd7da454da27927622f48bbac6990ac7a4c4a4653e7b0036/coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970", size = 254823, upload-time = "2026-07-02T13:09:30.177Z" }, + { url = "https://files.pythonhosted.org/packages/a9/09/dad6a75a2e561b9dc5086a8c5257a7591d584246f67e23e70d2995b89ab6/coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c", size = 256059, upload-time = "2026-07-02T13:09:31.979Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/b5d2941fa9564573d44b693a871ff3156f0c42cbefe977a09fa7fdc59971/coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84", size = 258190, upload-time = "2026-07-02T13:09:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1d/8e895bcde3c57ccd46d896dda5f2b3d5df761a1b0c6c9d450d175dedc632/coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389", size = 252456, upload-time = "2026-07-02T13:09:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/f6997da343ddeb959be82c3b05322793f92c071ad45f7cb8a96336e2dd5f/coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950", size = 254192, upload-time = "2026-07-02T13:09:37.445Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/a0bc09d032267b9da89d95a2d874cfbef2a5aebbf0e87cf7aba221d79a99/coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e", size = 252153, upload-time = "2026-07-02T13:09:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/54/c0/77fc233d9fba07b244c40948c53fe27308b8f21732fb3417f87fbd6fd992/coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e", size = 256310, upload-time = "2026-07-02T13:09:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/d5/24/601cecfb5825becacb8d45219a018a3b55b9dbaec624efdb0ea249d08be2/coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837", size = 251974, upload-time = "2026-07-02T13:09:42.733Z" }, + { url = "https://files.pythonhosted.org/packages/47/1e/6f45e5a5b3d5484318d368702af6716b5ab8913b0428bec981a562fcf296/coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37", size = 253745, upload-time = "2026-07-02T13:09:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4df027a77bd11d0e527f44c53557c76e54ad027413d0304252ea3a78d67e/coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b", size = 222902, upload-time = "2026-07-02T13:09:46.122Z" }, + { url = "https://files.pythonhosted.org/packages/a0/10/0355894d34e231f2c5449e71287e81a50793a325df2e2b027b7bcd9dfd19/coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629", size = 223444, upload-time = "2026-07-02T13:09:47.687Z" }, + { url = "https://files.pythonhosted.org/packages/06/ef/bb725f263befaaff851203ab338e68af15e195d7f7b5f323162532d9b6a8/coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21", size = 222839, upload-time = "2026-07-02T13:09:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9c/1e3ca54f72a3185ece06c58d871099898c48f0ed6430d17b6ab75f0d180a/coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324", size = 220906, upload-time = "2026-07-02T13:09:51.339Z" }, + { url = "https://files.pythonhosted.org/packages/09/37/f718613d83b274880382f6b67e78f3802549ae39b0b3e65ae5b5974df56e/coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8", size = 221239, upload-time = "2026-07-02T13:09:53.138Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/22bae91e0b75445f68d365c7643ed0aa4880bbf77450ee74ca65bdae53a7/coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092", size = 252286, upload-time = "2026-07-02T13:09:54.996Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/bec5e32aa508615d9d7a2790effb25fb4dc28606e995816afe400b25ece3/coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502", size = 254789, upload-time = "2026-07-02T13:09:56.678Z" }, + { url = "https://files.pythonhosted.org/packages/17/29/0e865435b4354e4a7c03b1b7920046d31d0a273d55decefea27e011cb9bf/coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2", size = 256135, upload-time = "2026-07-02T13:09:58.343Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/33a870b58a13325d62fc0a6c8f01fa0ff667cef60c7498e2382a147dfa18/coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e", size = 258449, upload-time = "2026-07-02T13:10:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/18/7b/6fffe596bf3ddba8462758d02c5dad730fd91055a6634aa2e4226229181a/coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888", size = 252313, upload-time = "2026-07-02T13:10:01.946Z" }, + { url = "https://files.pythonhosted.org/packages/58/1b/11468dd6c1676ab831a70cb9a8d4e198e8607fa0b7220ab918b73fe9bfbd/coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859", size = 254142, upload-time = "2026-07-02T13:10:04.065Z" }, + { url = "https://files.pythonhosted.org/packages/79/41/29328e21d16b1b95092c30dd700e08cf915bd3734f836df8f3bdb0e8fa9f/coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99", size = 252108, upload-time = "2026-07-02T13:10:06.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/de/05ccfb990439655b35afbfd8e0d13fe66677565a7d4eb38c3f5ef2635e1c/coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676", size = 256385, upload-time = "2026-07-02T13:10:08.141Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/486828a3d2695ea7a2609f17ff572f6b01905e608379440a11da4b8dffbe/coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6", size = 251923, upload-time = "2026-07-02T13:10:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/18/c7/03582b6715f078e5e558354c87616d945b9894cda2dace8e4009b17035e4/coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe", size = 253580, upload-time = "2026-07-02T13:10:12.052Z" }, + { url = "https://files.pythonhosted.org/packages/db/dc/9e578bbaf2ecb4959a81b7e7601ad8cca772cba2892e8d144cb749b4a71a/coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f", size = 223107, upload-time = "2026-07-02T13:10:13.994Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3e/c8c3b75d8dbe0e35f7b0cc3ff5e949fc59500f70b21d0398813f66740664/coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663", size = 223597, upload-time = "2026-07-02T13:10:15.906Z" }, + { url = "https://files.pythonhosted.org/packages/cd/bc/3cbc9fb036eb388519bccd521f783499c39b64256013fbc362782f196fe1/coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da", size = 223020, upload-time = "2026-07-02T13:10:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/28/00/199c4a8d656dff63102577a056c0fce2ff6a79e40adac092fc986c49cbf1/coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641", size = 221638, upload-time = "2026-07-02T13:10:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/9d0092c96a3d3a26951ecc7020826aa57bcb1b119ca81acbba996884ab13/coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa", size = 221903, upload-time = "2026-07-02T13:10:21.514Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b4/c0ca3028f42c9a08e51feb4561ef1192e5de99797cd1db5b04590c215bda/coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461", size = 263267, upload-time = "2026-07-02T13:10:23.261Z" }, + { url = "https://files.pythonhosted.org/packages/5f/aa/a375e3846e5d3c013dc600b2a3231089055c73d77f5393dd2192a8d64da6/coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72", size = 265390, upload-time = "2026-07-02T13:10:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/92/e1/5783cdabb797305e1c9e4809fea496d31834c51fa772514f73dc148bcfc9/coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd", size = 267811, upload-time = "2026-07-02T13:10:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/85/31/96d8bbf58b8e9193bc8389574a91a0db48355ee98feb66aa6bf8d1b32eea/coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5", size = 268928, upload-time = "2026-07-02T13:10:29.242Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7a/5294567e811a1cb7eda93140c628fa050d66189da28da320f93d1d815c73/coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007", size = 262378, upload-time = "2026-07-02T13:10:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/69/3f/3f48538421f899f28946f90a3d272136a4686e1abf461cc9249a783ee0f3/coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9", size = 265263, upload-time = "2026-07-02T13:10:32.942Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d3/092df15efcab8a9c1467ee960eb8019bbad3f9300d115d89ea6195f369ff/coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f", size = 262866, upload-time = "2026-07-02T13:10:35.104Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ab/0254d2b88665efb2c57ad368cc77ab5de3435bd8d5add4729c1b0e79431e/coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571", size = 266599, upload-time = "2026-07-02T13:10:37.05Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/1cfa4023e489ce6fbc7be4a5d442dbc375edb4f4fda39a352cedb53263c2/coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8", size = 261714, upload-time = "2026-07-02T13:10:38.966Z" }, + { url = "https://files.pythonhosted.org/packages/b7/eb/fee5c8665656be63f497418d410484637c438172568688e8ac92e06574e7/coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68", size = 264025, upload-time = "2026-07-02T13:10:40.789Z" }, + { url = "https://files.pythonhosted.org/packages/ab/99/63005db722f91edc81abc16302f9cc2f6228c1679e46e15be9ae144b14d0/coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b", size = 223413, upload-time = "2026-07-02T13:10:42.597Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e8/2bc6181c4fb06f1a6b981eb85330cc57bfad7e3f710fc9c9d350013ba228/coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080", size = 224245, upload-time = "2026-07-02T13:10:44.47Z" }, + { url = "https://files.pythonhosted.org/packages/79/b8/4d959bf9cc45d0cfed2f4d35cafcab978cdb6ea02eb5100009cd740632a3/coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5", size = 223558, upload-time = "2026-07-02T13:10:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, ] [[package]] name = "cyclonedx-python-lib" -version = "11.6.0" +version = "11.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "license-expression" }, @@ -387,39 +376,39 @@ dependencies = [ { name = "sortedcontainers" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/ed/54ecfa25fc145c58bf4f98090f7b6ffe5188d0759248c57dde44427ea239/cyclonedx_python_lib-11.6.0.tar.gz", hash = "sha256:7fb85a4371fa3a203e5be577ac22b7e9a7157f8b0058b7448731474d6dea7bf0", size = 1408147, upload-time = "2025-12-02T12:28:46.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/c9/5d0ccdd19bc7d8ab803b90695c1706aa2ea8529685d18e682dc2524d2630/cyclonedx_python_lib-11.11.0.tar.gz", hash = "sha256:4b3194db72b613717f2912447e67ab618c75ff7dcac6c4af3c0e9e1ac617c102", size = 1442983, upload-time = "2026-06-17T11:57:49.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/1b/534ad8a5e0f9470522811a8e5a9bc5d328fb7738ba29faf357467a4ef6d0/cyclonedx_python_lib-11.6.0-py3-none-any.whl", hash = "sha256:94f4aae97db42a452134dafdddcfab9745324198201c4777ed131e64c8380759", size = 511157, upload-time = "2025-12-02T12:28:44.158Z" }, + { url = "https://files.pythonhosted.org/packages/22/f3/56ccb2884aaa3db5622368e5191a3384b15f35392aa93df8b2f508c660d2/cyclonedx_python_lib-11.11.0-py3-none-any.whl", hash = "sha256:3049fc83e06a059b5c5907a527625a8ed5073caab10607ed4c9e5503b590fd44", size = 528689, upload-time = "2026-06-17T11:57:47.358Z" }, ] [[package]] name = "debugpy" -version = "1.8.20" +version = "1.8.21" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, - { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, - { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, - { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, - { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, - { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, - { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, - { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, - { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, + { url = "https://files.pythonhosted.org/packages/a2/df/bf625547431a9cadc9f4cbfeda38866e2b17f6aed147b625377e87834449/debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e", size = 2483609, upload-time = "2026-06-01T19:30:50.794Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/59324b903599031ff9faaec1758292409f6561a0ec2492fe4b703327705a/debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176", size = 3968900, upload-time = "2026-06-01T19:30:52.341Z" }, + { url = "https://files.pythonhosted.org/packages/14/cd/27f65b805d7fe005c44e1a36b9183ecdfbcdbf9d3e721a5115d461ecc7ee/debugpy-1.8.21-cp312-cp312-win32.whl", hash = "sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9", size = 5336340, upload-time = "2026-06-01T19:30:54.047Z" }, + { url = "https://files.pythonhosted.org/packages/77/1d/c84e30c0c674184948b66f076ab271c01d940618a2824c23cd035a27bc20/debugpy-1.8.21-cp312-cp312-win_amd64.whl", hash = "sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c", size = 5374751, upload-time = "2026-06-01T19:30:55.891Z" }, + { url = "https://files.pythonhosted.org/packages/77/6b/d817e1f8cc77aa055d37fba092e0febfdff40fe652d8d53d4cd7a86ad98d/debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88", size = 2477398, upload-time = "2026-06-01T19:30:57.644Z" }, + { url = "https://files.pythonhosted.org/packages/48/57/412421516afc3055fa577516f00beec3d663f9b0ab330639547ae6c57720/debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2", size = 3962096, upload-time = "2026-06-01T19:30:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2c616337cf6ba7b07ebbc97f02c6c945a8e2f76b365e33ee809c32ee36d1/debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1", size = 5336288, upload-time = "2026-06-01T19:31:00.79Z" }, + { url = "https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0", size = 5376567, upload-time = "2026-06-01T19:31:02.56Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" }, + { url = "https://files.pythonhosted.org/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" }, ] [[package]] name = "decorator" -version = "5.2.1" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] [[package]] @@ -446,11 +435,11 @@ wheels = [ [[package]] name = "distlib" -version = "0.4.0" +version = "0.4.3" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, ] [[package]] @@ -487,11 +476,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.25.0" +version = "3.29.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/ee/29c668c50888588c432a702f7c2e8ee8a0c9e5286028d91f170308d6b2e9/filelock-3.29.5.tar.gz", hash = "sha256:6e6034c57a00a020e767f2614a5539863f056de7e7991d6d1473aef7ff73f156", size = 68927, upload-time = "2026-07-03T03:50:31.818Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e3/f1fae3647d170919c2cf2a898e77e7d1a4e5c7cae0aed7bb4bd3f5ebff6f/filelock-3.29.5-py3-none-any.whl", hash = "sha256:8af830889ba3a0ffcefbd6c7d2af8a54012058103771f2e10848222f476a1693", size = 45073, upload-time = "2026-07-03T03:50:30.445Z" }, ] [[package]] @@ -533,20 +522,20 @@ coverage = [ [[package]] name = "identify" -version = "2.6.17" +version = "2.6.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, ] [[package]] name = "idna" -version = "3.17" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -584,7 +573,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.12.0" +version = "9.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -594,13 +583,14 @@ dependencies = [ { name = "matplotlib-inline" }, { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit" }, + { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, + { url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" }, ] [[package]] @@ -626,14 +616,14 @@ wheels = [ [[package]] name = "jedi" -version = "0.19.2" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, ] [[package]] @@ -677,7 +667,7 @@ wheels = [ [[package]] name = "jupyter-client" -version = "8.8.0" +version = "8.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, @@ -685,10 +675,11 @@ dependencies = [ { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, ] [[package]] @@ -706,62 +697,64 @@ wheels = [ [[package]] name = "librt" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, - { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, - { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, - { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, - { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, - { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, - { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, - { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, - { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, - { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, - { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, - { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, - { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, - { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, - { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, - { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, - { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, - { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, - { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, - { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, - { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, - { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, - { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/e0/dbd0f2a68a1c1a1991eb7921ff6014465d56608cdc9a9fb468a616210a37/librt-0.12.0.tar.gz", hash = "sha256:cb26faedbd09c6130e9c1b64d8000efec5076ffd18d606c6cd1cf02730e6d8b0", size = 203841, upload-time = "2026-06-30T16:14:29.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1a/5bec493821b0e85b91de4f234912b50133d1aedb875048eef27938ec3f96/librt-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9bce19aa7c05f91c989f9da7b567f81d21d57a2e6501e2b811aa0f3f79614c1a", size = 146756, upload-time = "2026-06-30T16:12:44.395Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d0/cc04b48a57c1f275387f5578847214c4a6c21bfb24c6c8c8d6ba753fe403/librt-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0ace09f5bf4d982fe726015f102fb856658b41580597104e301e630ed1d8d86", size = 145537, upload-time = "2026-06-30T16:12:45.95Z" }, + { url = "https://files.pythonhosted.org/packages/9e/10/c02325556beb2aa158c9e549ddade8cc9a23b36cdad14756dbed730c1ff1/librt-0.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d007efe9243ede81ce75990ad7aa172da1e2024144b3eff17ba46a5fff1fff3c", size = 488637, upload-time = "2026-06-30T16:12:47.658Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9e/7b49ca1c30baa9c8df96024aa09a97c35a97455e36004c9b5311703c56f3/librt-0.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ad324a5e4858388a4864915b90a42efc8b374376393f14b9940f2454e791912b", size = 483651, upload-time = "2026-06-30T16:12:49.283Z" }, + { url = "https://files.pythonhosted.org/packages/4d/71/03c8c8cec39645fda451132ff9d6d662fc5aea42a1a188a77a4fddb35906/librt-0.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10a40cf74cdd97b6f8f905056db73f5d459783de2ca04c6ebd1bf47652818e7e", size = 518359, upload-time = "2026-06-30T16:12:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ec/a9f357f94bbcba92277d22af22cff42ef706ae5d9d6d58b69bebf3a67954/librt-0.12.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:92e61c09de95217ae02a9d17f4f66cf073253cdc51bcfdc0f15c62c9a70baa85", size = 509510, upload-time = "2026-06-30T16:12:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/717055325d028743aa01a7691ad59a63352a26a8ff2e7eeb0c9249514150/librt-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0461344061d6fc3718940f5855d95647831cef6d03a6c7506897f98222784ad4", size = 527302, upload-time = "2026-06-30T16:12:54.244Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/7612eeedb3395d92f7c6a84dca5f15e282d650483a4dc01aa5b9cffdfda3/librt-0.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e6dfe89074732c9287b3c0f5a6af575c9ede380a788013876cc7b14fe0da0361", size = 532568, upload-time = "2026-06-30T16:12:55.74Z" }, + { url = "https://files.pythonhosted.org/packages/79/1e/a9afe85d5bb8b65dc27be3809ed1d69082079e1e9717fd2c66aa9939600c/librt-0.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9efed79d51ad1383bba0855f613cca7aa91c943e709af2413ac7f4bb9936ce08", size = 521579, upload-time = "2026-06-30T16:12:57.884Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/93aebb219d52c37ea578f83b0588cd7b040974e464d4e435086a48b4dc4d/librt-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1eac6cc0e23e448fb3c1446ed85ff796afb616eed5897c978d35dbec030b7c7c", size = 558743, upload-time = "2026-06-30T16:12:59.577Z" }, + { url = "https://files.pythonhosted.org/packages/3c/85/1680c0ec332f238e3145c5608d313ab0a43281e210a5dd87e3bc3cc25631/librt-0.12.0-cp312-cp312-win32.whl", hash = "sha256:0ab8ee0210047ae86ca023ccfbfe3df82077fd1c9bc021aebbf37d993ef64af0", size = 99200, upload-time = "2026-06-30T16:13:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/30/0e/abca12d8904875aa2ad66327390a3f7b1b75ebc43c0a00fc763cecf32ea5/librt-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:51c8bfa12632c81b94401c101bcedd0c56c3a1f8fa3273ca3472b28cd2f54003", size = 119390, upload-time = "2026-06-30T16:13:02.493Z" }, + { url = "https://files.pythonhosted.org/packages/32/a5/4203481b6d3a3bb348c82ac71abf1fcb4cb3ae8422a24a8dee4cd3ac5bd7/librt-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:5eebd451f5def089369ba6d8ff0291303d035e8154f9f26f7633835c5b029ade", size = 105117, upload-time = "2026-06-30T16:13:03.952Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/568d948c8079c9ff3c9e8110cf85f1eb70218e1209af29d0b7b89aa4a60c/librt-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8d9a55760a34ae5ce70434aabb6a6c61c6c44a0ec58ca1cfd9cd86e4745d417d", size = 146808, upload-time = "2026-06-30T16:13:05.417Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/bea471ecea210088847bb5f3c4b4b424d596518934c06679b78ca85d6e63/librt-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff0b197e338b4cf432873e0d6ef025213fdea85311ec4d87d2ea88c28adf2409", size = 145503, upload-time = "2026-06-30T16:13:07.023Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9e/984ad422b56de95fdce158f06b051655373784ebea0aba9a7fcbc41614d1/librt-0.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e69f120a20b69e2539d603bbd4d62db38399b10f8bf73a1cf445038a621e8af", size = 488421, upload-time = "2026-06-30T16:13:08.492Z" }, + { url = "https://files.pythonhosted.org/packages/50/03/1a2f94009b07ea71f8e1a4cfe53370565b56da9caa341b89e0699325e9f5/librt-0.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fde3cde595e947fc8e755b0a21f919a1622483d07c662d00496e040773d22591", size = 483488, upload-time = "2026-06-30T16:13:10.169Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3b/084bdc295823fbb6ab91670047adf8f420787f9e8794bf2d140b66dc196b/librt-0.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d977447315fa09ea4e8c7ae9b4e22f7659b5128161c1fd55ff786b5349f73503", size = 518428, upload-time = "2026-06-30T16:13:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/c9/22/5a307390b93a115ffbecd95c64eecb4e56269680e45e9415ada7285f2cf4/librt-0.12.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ffac8a67e4143cea9a549d4822b93bc0bbaad73fc25aa0ab0ba5ec27d178677", size = 509744, upload-time = "2026-06-30T16:13:13.217Z" }, + { url = "https://files.pythonhosted.org/packages/b5/90/83f3cb6184f5d669660717b4b2e317c9ddaccf7ca5bb97f2196deac1a3b7/librt-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:94af1ed773ff104ef08ef3d669a0ba9d3a5916c609eb698cffe5d5476d66ff9b", size = 527749, upload-time = "2026-06-30T16:13:15.277Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3b/f162be5cc88d47378e3a20776fe425fa1c2bece755da15e2783ebf06d3d6/librt-0.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:548199d21d22fb26398dfbbe0ba953a52465c66f3a49f38e6fddce1b127faf53", size = 532582, upload-time = "2026-06-30T16:13:17.074Z" }, + { url = "https://files.pythonhosted.org/packages/c9/28/6c5d2f6b7232fd24f284fc4cab37a459fe69a9096a09942f44cc5c55e073/librt-0.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c8f1f413b966a9dd3ecf80cd337b0ad7bb3de2474a4ff448ed3ebabfc3f803fc", size = 522235, upload-time = "2026-06-30T16:13:18.823Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1c/bd115360587fdc22c8ae8fac14c040a556b442e2965d4370d2cf274c8b95/librt-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55f13f95b629be5b6ab38918e439bf14169d6f9a8deaae55e0c14e12fb0c74b9", size = 559055, upload-time = "2026-06-30T16:13:20.509Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5a/c26f49f576437014825a86faea3cec60c1ed17f976abd567b6c12b8e35a7/librt-0.12.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:8b2dc079dfe29e77a47a19073d2040fa4879aa3656501f1650f8402ddce0313c", size = 79809, upload-time = "2026-06-30T16:13:22.401Z" }, + { url = "https://files.pythonhosted.org/packages/69/0b/a55244261d9ad7375ac039b8af06d42602722e2e8b8d8d6b86e4a3888c02/librt-0.12.0-cp313-cp313-win32.whl", hash = "sha256:da58944be8270f2bfee628a9a2a60c1cf6a12c8bea8e2c9b6edf3e5414ca7793", size = 99308, upload-time = "2026-06-30T16:13:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bf/ed9465e58d44c5a5637795547d0841c8934aab905ea452cac1adf14672cf/librt-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:1db4be3037e4ce065a071fa7deee93e78ebc25f448340a02a6c1c0b82c37e383", size = 119438, upload-time = "2026-06-30T16:13:25.188Z" }, + { url = "https://files.pythonhosted.org/packages/c0/44/3cad652aeb892e6e8ffe48d0fafa2bc652f28ec7ed3f4403fcbb1be4f948/librt-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:05fd2542892ad770b5dd45003fd080477cf220b611d3ee59b0792097eb0873a9", size = 105118, upload-time = "2026-06-30T16:13:26.533Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/3a0e05618c12423b6fc5141b590ec02a6efb645833edc8736a6c7b46d1ec/librt-0.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b37ee42e09722284a6d9288fe44a191f7276060a3195939bb77c6502058dbb34", size = 145579, upload-time = "2026-06-30T16:13:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/77/9e/fd399d099dfb4020f3f7c34e7e6210c389fa89f7d79ca92f5afb0395f278/librt-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ade11988728b3e4768dadc5696e82c60e9b35fc95335a9b4d1f5d69e753ccec7", size = 150139, upload-time = "2026-06-30T16:13:29.357Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ee/610239fbd8c4b005443664c5d4c3bc1717daedd8c71369bf45011aa87194/librt-0.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f351ed425380e39bd86df382578aa5b8c5b98e2e265112de7379e7d030258150", size = 480457, upload-time = "2026-06-30T16:13:30.78Z" }, + { url = "https://files.pythonhosted.org/packages/0c/10/ceddc9010f26c541444be36e1153a79b64626694db2d33a524c719fa3e46/librt-0.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:857d2163e088c868967717ace8e980017fd868a735f3de010412af02bdc30319", size = 479002, upload-time = "2026-06-30T16:13:32.398Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/b1523d9718e8192e5403e6b41a02742e17ba554369f0729b9f30ab590e2d/librt-0.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2befc80aa5f2f5b93f28abaaf11feff6677931dd548320e44c52deaa9399744", size = 510527, upload-time = "2026-06-30T16:13:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0e/0f3ff43befb18a531615736791e52fb67eaa71ff7b89e6e5f7004b64cc6e/librt-0.12.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:be3694dcfa97c6715dd19ac73d3e1b21a805514a5785663e57fecacd3ff64e5a", size = 500988, upload-time = "2026-06-30T16:13:36.408Z" }, + { url = "https://files.pythonhosted.org/packages/a8/1a/0278ea4a9e599dc507c43839a87f2c764ad04bf69418e2d763d58659e55f/librt-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2d5f67e86f45638843d025b0828f2e9e55fc45ff9180d2618ccdeaf72a796050", size = 519318, upload-time = "2026-06-30T16:13:37.883Z" }, + { url = "https://files.pythonhosted.org/packages/59/55/090e10e62be2f35265e41601337f83ac9f83be9aca1bf92692e3a82effdd/librt-0.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:64572c85e4ab7d572c9b72cd76b5f90b21181b1459fa6b1aac6f8958c4fcff31", size = 527127, upload-time = "2026-06-30T16:13:39.682Z" }, + { url = "https://files.pythonhosted.org/packages/1f/34/8052c9ec678be6ba751279947831f089aa69b009000b985ce91d1979669a/librt-0.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8b961912b0e688c1eb4658a46bdb0606b31918d65597fbe7356ca83aa653ffcc", size = 509766, upload-time = "2026-06-30T16:13:41.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f8/8761b36189e9ec8dc20b49fa84cef22852c6c41fcda56f760f7fc1360da5/librt-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:722375903e3f079436a7a33da51ce73931536dd041f9feb01536f05d8e010c96", size = 552043, upload-time = "2026-06-30T16:13:43.197Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/7283971ef6b70269938b49c7b25f670ec6325d252265fbcc996f9b364379/librt-0.12.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:a5a96a8f536b65ef1bf910c09e7e71647edde5111f6e1b51f413c6fba5bfe71b", size = 79472, upload-time = "2026-06-30T16:13:44.64Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5e/b30940dea935e8ac5bd0e0abb1985f5274590d557ac3a252ca0d5392ce52/librt-0.12.0-cp314-cp314-win32.whl", hash = "sha256:8ffc99c356f1777c506e1b69dc303879153ae2640ba15b8f3d4448bc87139149", size = 94246, upload-time = "2026-06-30T16:13:45.962Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4e/0af9fe63f35fa304da3b05688f30ff6a329bcc59581b1cc51dc87fd30141/librt-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:1e68fb20798f455cda41d20a306a23c901218883f17a4bab1ed6e1331b265fb7", size = 114951, upload-time = "2026-06-30T16:13:47.279Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8e/843c495d7db35e13b84cd533898fa89145c40dc255da0bc316d53d631464/librt-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:2df534f97916cf38ec9b1ddafeb68ae1a4cd4a54775ff26a797026774c0517cf", size = 100562, upload-time = "2026-06-30T16:13:48.699Z" }, + { url = "https://files.pythonhosted.org/packages/75/30/c686d0f978d5fd6867c5bbad96b015c9445746764d1c228e16a2d30d9382/librt-0.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c09e581b1c2b8a62b809d4f4bd101ca3de93791e5b0ed1a14085d911be3dee3f", size = 153897, upload-time = "2026-06-30T16:13:50.017Z" }, + { url = "https://files.pythonhosted.org/packages/40/46/f6f2d77ce46628b48fb5280709013b5109cf3a2c46a2472093cdfc03519d/librt-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:976888d0d831402086e641018bcc3208e0a38f0835789da91f72894b2cb4161f", size = 156391, upload-time = "2026-06-30T16:13:51.462Z" }, + { url = "https://files.pythonhosted.org/packages/c2/46/cd790c7e19e460779471530ffab454541d6ea4a3b7d338cad7f16ff96995/librt-0.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563c37cdb41d08fe1e3f08b201abac0e317ca18e88b91285466ee0a585797520", size = 564151, upload-time = "2026-06-30T16:13:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/724559a15fb023cbdef7aee1e81fbfbc3ee22fd09009baa816cea63e3a60/librt-0.12.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b97eb1a3140e279cc76f85b0fb92b7eb3dfbe0471260ee878bc9dc4bf9a0d649", size = 546002, upload-time = "2026-06-30T16:13:54.665Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7e/f9d8c257ab4909f101c7c13734367749e782fd8625545f0343502c2f09f1/librt-0.12.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06e0623351ab9904cf628245f99c714586f4dd23dc740b88c8bc670d8401a847", size = 584204, upload-time = "2026-06-30T16:13:56.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/33/64665810575ac23b6cb6ef364de51309b7803620c12885b6e895ebc29591/librt-0.12.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da12f017b2e404554be14d466cd992459feaa44f252b0f18d909a85266ce1237", size = 573688, upload-time = "2026-06-30T16:13:58.1Z" }, + { url = "https://files.pythonhosted.org/packages/0f/01/27522995c6627455abc7a939d57535fb1a7836d398ccedb3d7585f46039e/librt-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d97f31003a5c86b9e78155a829572c3a26484064fb7ac1d9695fe628bd93d029", size = 604719, upload-time = "2026-06-30T16:13:59.831Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1f/099e61b1b688551d6d2ce9d4d2ae2242a938759db8551e6cbac7f7176ee5/librt-0.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:bd43a6c69876aef4f04eaae3d3b99b0be64755fda274002fa445b92480bf664e", size = 598183, upload-time = "2026-06-30T16:14:01.457Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c1/050400249665503bdd5b83cec518fa7b183b609341c8dcd58161775c4226/librt-0.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c01755c72fca1dc6b8d5c2ed228b8e7b2ffe184675c22f0f05ebd8fe188b9250", size = 582559, upload-time = "2026-06-30T16:14:03.29Z" }, + { url = "https://files.pythonhosted.org/packages/da/d1/eef8f0e6722518b65a3d3bcd9309f9f44e208ce5d6728070820f988e7078/librt-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:625ae561d5fa36400856dcc27464400d047bc2d5e3446be88f437b03fefd72e4", size = 626375, upload-time = "2026-06-30T16:14:04.957Z" }, + { url = "https://files.pythonhosted.org/packages/8b/78/f0bb41a6f2bbd3c77bdcc66980dc0d69ca1192a0ecec25377afcc5e6db73/librt-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8d73191883553ee0739741544bf3b00aba2a1224e45d9580b30cbc29e21dc03b", size = 97752, upload-time = "2026-06-30T16:14:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/92/24/e279c27972ab051a070237cfa45728fa51670c3f22f1a4d391711e9f4c31/librt-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e1cbb037324e759f0afa270229731ff0047772667f3cb38ef5df2cabf0175ede", size = 119562, upload-time = "2026-06-30T16:14:07.908Z" }, + { url = "https://files.pythonhosted.org/packages/06/e6/42a475bfca683b0cd5366f6dd06580062b7e567bb8534d225c877c2f14f3/librt-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bca1472acbd473eff61059b4409f802c5a1bcb4cd0344d06f939df9c4c125d40", size = 104282, upload-time = "2026-06-30T16:14:09.29Z" }, ] [[package]] @@ -778,14 +771,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -853,14 +846,14 @@ wheels = [ [[package]] name = "matplotlib-inline" -version = "0.2.1" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] [[package]] @@ -926,7 +919,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "1.20.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -934,27 +927,37 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, ] [[package]] @@ -1022,29 +1025,29 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "parso" -version = "0.8.6" +version = "0.8.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] @@ -1061,71 +1064,73 @@ wheels = [ [[package]] name = "pillow" -version = "12.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, ] [[package]] @@ -1151,7 +1156,7 @@ wheels = [ [[package]] name = "pip-audit" -version = "2.10.0" +version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachecontrol", extra = ["filecache"] }, @@ -1165,9 +1170,9 @@ dependencies = [ { name = "tomli" }, { name = "tomli-w" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/89/0e999b413facab81c33d118f3ac3739fd02c0622ccf7c4e82e37cebd8447/pip_audit-2.10.0.tar.gz", hash = "sha256:427ea5bf61d1d06b98b1ae29b7feacc00288a2eced52c9c58ceed5253ef6c2a4", size = 53776, upload-time = "2025-12-01T23:42:40.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/a4/f21d5f0a0edabcbce31560b73c7c5a6f72ae87af4236fd1069c8f59a353d/pip_audit-2.10.1.tar.gz", hash = "sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc", size = 54275, upload-time = "2026-06-10T22:17:01.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl", hash = "sha256:16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f", size = 61518, upload-time = "2025-12-01T23:42:39.193Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a7/b0c504148114047bd1bc9d97447453c6850ca176bb2f3c0038835994e8b7/pip_audit-2.10.1-py3-none-any.whl", hash = "sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a", size = 62023, upload-time = "2026-06-10T22:17:00.309Z" }, ] [[package]] @@ -1185,11 +1190,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.2" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] @@ -1203,7 +1208,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.5.1" +version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -1212,9 +1217,9 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] [[package]] @@ -1328,7 +1333,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1337,9 +1342,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -1357,16 +1362,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -1453,15 +1458,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.1.0" +version = "1.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/26/8b004cc36f430345136f6f00fa1aa9ed596c8ed1e8504625fa79522ff39c/python_discovery-1.4.3.tar.gz", hash = "sha256:ad57d7045a862460d4a235986c33f13ed707d3aeb9153fa47eb7dfd0d4673289", size = 70438, upload-time = "2026-07-03T13:21:51.621Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/9b77ecb4644d1bbea94d29abf78f21c47eca6eb79e9745b702ec0bed2e19/python_discovery-1.4.3-py3-none-any.whl", hash = "sha256:b6e1e4a7d9e3f6948c39746ffe8218225162d738ba39d05ab1d2f6c1cac4878c", size = 33885, upload-time = "2026-07-03T13:21:50.174Z" }, ] [[package]] @@ -1516,18 +1521,21 @@ wheels = [ [[package]] name = "pywin32" -version = "311" +version = "312" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] [[package]] @@ -1635,7 +1643,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1643,137 +1651,152 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] name = "rich" -version = "14.3.3" +version = "14.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, ] [[package]] name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, ] [[package]] name = "ruff" -version = "0.15.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" }, - { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" }, - { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" }, - { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" }, - { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" }, - { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" }, - { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" }, - { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" }, - { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" }, - { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" }, - { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] [[package]] name = "setuptools" -version = "82.0.0" +version = "83.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, ] [[package]] @@ -1787,11 +1810,11 @@ wheels = [ [[package]] name = "snowballstemmer" -version = "3.0.1" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, ] [[package]] @@ -1819,11 +1842,11 @@ wheels = [ [[package]] name = "stevedore" -version = "5.7.0" +version = "5.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/dd/04d56c2a5232358df41f3d0f0e31833d378b6c8ed7803a6b1b7867b0eba6/stevedore-5.9.0.tar.gz", hash = "sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c", size = 514850, upload-time = "2026-07-02T11:38:08.509Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, + { url = "https://files.pythonhosted.org/packages/62/8d/008761f6e1000600e5303db30d05724bdcf3d2d186cbb59fac79b52e39ed/stevedore-5.9.0-py3-none-any.whl", hash = "sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7", size = 54463, upload-time = "2026-07-02T11:38:07.43Z" }, ] [[package]] @@ -1853,47 +1876,47 @@ wheels = [ [[package]] name = "tomli" -version = "2.4.0" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] @@ -1924,34 +1947,34 @@ wheels = [ [[package]] name = "traitlets" -version = "5.14.3" +version = "5.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.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" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "virtualenv" -version = "21.1.0" +version = "21.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -1959,92 +1982,92 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/ff/3dcc01dab91e5d472183dd8d1ac0368b44b70aa3e678a3e47149e8ef716a/virtualenv-21.5.2.tar.gz", hash = "sha256:465da6db2a5b615258cddefb749ceeb3627e4fddda93be88c97ba8321ca3c6d1", size = 5525810, upload-time = "2026-07-06T15:21:23.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, + { url = "https://files.pythonhosted.org/packages/9d/08/0ae1145a8e8d7193f7f37aedc8e37eafd02f70a8513b90624089a7a8e321/virtualenv-21.5.2-py3-none-any.whl", hash = "sha256:4d9f76b4363ed04a4837e812110eb301367eb470f616b62399a50d39dee70e47", size = 5506034, upload-time = "2026-07-06T15:21:20.848Z" }, ] [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] [[package]] name = "werkzeug" -version = "3.1.6" +version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] [[package]] name = "wrapt" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, - { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, - { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, - { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, - { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, - { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, - { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, - { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, - { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, - { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, - { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, - { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, - { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, - { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, - { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, - { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, - { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, - { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, - { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, - { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, - { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, - { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, - { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, - { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, - { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, - { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, - { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, - { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, - { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, - { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, - { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, - { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/85/180b40628b23772692a0c76e8030114e1c0ae068470ed531919f0a5f2a4a/wrapt-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8417fd3c674d3c8023d080292d29301531a12daf8bd938dd419710dd2f464f2b", size = 81484, upload-time = "2026-06-20T23:47:59.924Z" }, + { url = "https://files.pythonhosted.org/packages/94/f2/21c90f2a16689702e2aaff45795b11018dff2c9b1242bac10d225483f676/wrapt-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7070c7472582e31af3dfc2622b2381a0df7435110a9388ed8db5ffbce67efb", size = 82151, upload-time = "2026-06-20T23:48:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b3/7e6e9fcf4fe7e1b69a49fe6cc5a44e8224bab6283c5233c97e132f14908e/wrapt-2.2.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e096c9d39a59b35b63c9aacfbbbec2088ff51ff1fc31051acc60a07f42f273a", size = 169828, upload-time = "2026-06-20T23:48:02.719Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/894f132d857ed5a9904d937baf368badcbe5ea9e436e2f1930fe21c9f1f0/wrapt-2.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d1a6050405bf334be33bf66296f113563622972a34900ae6fa60fd283a1a900", size = 171544, upload-time = "2026-06-20T23:48:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/29/de/3c833e03725b477e9ea34028224dd21a48781830101e4e036f77e8b6b102/wrapt-2.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10adb01371408c6de504a6658b9886480f1a4919a83752748a387a504a21df79", size = 160663, upload-time = "2026-06-20T23:48:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/be/27edce350b24e3054d9d047f65f16d4c4d4c1f3f31c4278a1f8a95c723c8/wrapt-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3442eee2a5798f9b451f1b2cd7518ce8b7e28a2a364696c414460a0e295c012a", size = 169387, upload-time = "2026-06-20T23:48:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c4/9fd9679af8bf38e146652c7f47b6b352c3e5795b4ad1c0b7f94e15ac2aa7/wrapt-2.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6c99012a22f735a85eed7c4b86a3e99c30fdd57d9e115b2b45f796264b58d0bf", size = 158849, upload-time = "2026-06-20T23:48:08.91Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c2/aa6c0c2206803068c6859dabe01f8c84c43744da93d4c67b8946d21655ee/wrapt-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b686cfc008776a3952d6213cb296ed7f45d782a8453936406faa89eac0835ab", size = 168147, upload-time = "2026-06-20T23:48:10.374Z" }, + { url = "https://files.pythonhosted.org/packages/42/63/3eb25da41049d20ae18fcab2dd8b056e02387c4bfa626cbdfb7c3b872e4f/wrapt-2.2.2-cp312-cp312-win32.whl", hash = "sha256:ef2cce266b5b0b07e19fa82e59673b81142b7a3607c8ed1254113d048ed668da", size = 77734, upload-time = "2026-06-20T23:48:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/0390e008a305360948fa9ce69507d041ac12cb2ee5d28e34467e2ee79391/wrapt-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:abf8c20a2d72ee69e16328b3c91342c446e723bfe48bfcc4dded3b9722ac027f", size = 80585, upload-time = "2026-06-20T23:48:13.117Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b3/84c445c66969f2d3457276b183a48c91097d59bbef9af6c075366b0f8c36/wrapt-2.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:c6c64c5d02578bc4c4bca4f0aef1504de933c1d5b4ac2710b9131111459506c8", size = 79553, upload-time = "2026-06-20T23:48:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/43/fc/f32f4b22c6511173c11d9e541ab4e7d8467a0f1b3455acaf784115d31ff8/wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69", size = 81296, upload-time = "2026-06-20T23:48:15.881Z" }, + { url = "https://files.pythonhosted.org/packages/72/06/4d117d5d77a9344776c0248b24dae3d3dd2f58e5f765fa08cf887072e719/wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617", size = 81841, upload-time = "2026-06-20T23:48:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/15/ff/63ad96f98eb58a742b1a20d80f21da88924405910149950b912368150468/wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e", size = 167882, upload-time = "2026-06-20T23:48:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/8bb62d8933df7acf3247194e6e9fc68edf9d2fa203252c89c94b319dd472/wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d", size = 167411, upload-time = "2026-06-20T23:48:20.315Z" }, + { url = "https://files.pythonhosted.org/packages/17/09/8789dcb09ee1de715727db7521aabbb68ffa68dfade3a49468440cfced49/wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b", size = 158607, upload-time = "2026-06-20T23:48:21.728Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/66e02562d53ee67d841f175e38e3c993c2d78a3e104c576cad61c028b43c/wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c", size = 166367, upload-time = "2026-06-20T23:48:23.177Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a3/832ac4e41222fb263b3042d42c2f08d305db7d0f0c9b1d3a271a9eede8f6/wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f", size = 157176, upload-time = "2026-06-20T23:48:24.711Z" }, + { url = "https://files.pythonhosted.org/packages/b7/01/1bd5e4d2df9c0178989ac8da9186543465388588ee2ef153e2591accebef/wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94", size = 167025, upload-time = "2026-06-20T23:48:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/1c/69/583ed25291ab53e1ec117135fb1c33425e2f46d2bc8f29c17f7a94cf4274/wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d", size = 77605, upload-time = "2026-06-20T23:48:27.643Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/e69fc6d06e1523c68e0d00f95c9aed1158ce9908ee41603f7f2eae3d5db6/wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4", size = 80508, upload-time = "2026-06-20T23:48:29.013Z" }, + { url = "https://files.pythonhosted.org/packages/55/21/fe7a393d9e5dc0923bed8f5d857e9dcff210f1fa0888c02cc8f3ffaa55aa/wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac", size = 79565, upload-time = "2026-06-20T23:48:30.429Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e5/c120d13bf5091164f68c3c1657e84f16f57e71d978421b626393ac5bd7eb/wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5", size = 83264, upload-time = "2026-06-20T23:48:31.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b0/d4a1eb97e0e286625bdf21bc7f702637f9607787ffbbdb5ec14d50c79dbf/wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec", size = 83791, upload-time = "2026-06-20T23:48:33.482Z" }, + { url = "https://files.pythonhosted.org/packages/18/1e/f060df47755e87b57684cee7bfc1362b204df55fac96ffebc0631b697b79/wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9", size = 203399, upload-time = "2026-06-20T23:48:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/c4/de/2316a757a1abb6453700b79d83e532146dcef2611348282d4d8889792161/wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c", size = 210461, upload-time = "2026-06-20T23:48:36.569Z" }, + { url = "https://files.pythonhosted.org/packages/ed/29/d1160785ae18ca2495a6d82a21154103d74f656c9fd457fb35f6b11b965a/wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194", size = 195313, upload-time = "2026-06-20T23:48:38.175Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2d/7caa9598ae61a9cf0989cc501739cbeeb7d650ab3193cca1407b9af0c6ab/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066", size = 206116, upload-time = "2026-06-20T23:48:39.804Z" }, + { url = "https://files.pythonhosted.org/packages/ac/02/281ea1088b8650d865f311b35cf86fd21df89128e2909714f1161e01c9d0/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20", size = 192668, upload-time = "2026-06-20T23:48:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/be/7d/976e2d5b4b5c5babda40974edd54d0a5585cb60132ed86b46f4b80239b16/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af", size = 198891, upload-time = "2026-06-20T23:48:43.056Z" }, + { url = "https://files.pythonhosted.org/packages/59/b7/e47651797c097f75a37e2ce86dcf04048ff576f3a674f7c558df7b5e9622/wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9", size = 78537, upload-time = "2026-06-20T23:48:44.509Z" }, + { url = "https://files.pythonhosted.org/packages/d1/6f/9fa5d59fb06d890defb5a8f727ce6a14d2932c8760153f96956628559fee/wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28", size = 82005, upload-time = "2026-06-20T23:48:46.391Z" }, + { url = "https://files.pythonhosted.org/packages/15/80/4c7bd9873d1f9f7d138d93556b500469dbe24f42710b877519c2b9eb380d/wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0", size = 80762, upload-time = "2026-06-20T23:48:47.964Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745", size = 81341, upload-time = "2026-06-20T23:48:49.39Z" }, + { url = "https://files.pythonhosted.org/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00", size = 81921, upload-time = "2026-06-20T23:48:50.986Z" }, + { url = "https://files.pythonhosted.org/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc", size = 167713, upload-time = "2026-06-20T23:48:52.598Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95", size = 166779, upload-time = "2026-06-20T23:48:54.33Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33", size = 158407, upload-time = "2026-06-20T23:48:55.944Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab", size = 166594, upload-time = "2026-06-20T23:48:57.617Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663", size = 157068, upload-time = "2026-06-20T23:48:59.171Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d", size = 166470, upload-time = "2026-06-20T23:49:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56", size = 78062, upload-time = "2026-06-20T23:49:02.327Z" }, + { url = "https://files.pythonhosted.org/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406", size = 80832, upload-time = "2026-06-20T23:49:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c", size = 80029, upload-time = "2026-06-20T23:49:05.237Z" }, + { url = "https://files.pythonhosted.org/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a", size = 83357, upload-time = "2026-06-20T23:49:06.632Z" }, + { url = "https://files.pythonhosted.org/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063", size = 83794, upload-time = "2026-06-20T23:49:08.098Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f", size = 203362, upload-time = "2026-06-20T23:49:09.811Z" }, + { url = "https://files.pythonhosted.org/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81", size = 210449, upload-time = "2026-06-20T23:49:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c", size = 195349, upload-time = "2026-06-20T23:49:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff", size = 206099, upload-time = "2026-06-20T23:49:15.051Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5", size = 192728, upload-time = "2026-06-20T23:49:16.854Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c", size = 198842, upload-time = "2026-06-20T23:49:18.504Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca", size = 79059, upload-time = "2026-06-20T23:49:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77", size = 82462, upload-time = "2026-06-20T23:49:21.631Z" }, + { url = "https://files.pythonhosted.org/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347", size = 81182, upload-time = "2026-06-20T23:49:23.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, ] From 040b6dfdb68562aa8bebe8f13d5076a684bd732e Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Mon, 6 Jul 2026 17:39:20 +0200 Subject: [PATCH 158/199] refactor: enhance run-pip-audit script for improved error handling and ignore list management --- pyproject.toml | 4 -- scripts/hooks/run-pip-audit.sh | 116 ++++++++++++++++++++++++++++----- 2 files changed, 101 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2afbf0b8..971e053a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -194,10 +194,6 @@ skips = ["B101", "B601"] # Used by both pre-commit hooks and CI workflows via scripts/hooks/run-pip-audit.sh # See docs/6_pre_commit_troubleshooting.md for explanation of each exclusion ignore = [ - "CVE-2026-3219", # pip 26.0.1 pip handles concatenated tar and ZIP files as ZIP files regardless of filename or whether a file is both a tar and ZIP file. This behavior could result in confusing installation behavior, such as installing "incorrect" files according to the filename of the archive. New behavior only proceeds with installation if the file identifies uniquely as a ZIP or tar archive, not as both. - "CVE-2026-6357", - "CVE-2026-44431", # urllib3: Cross-origin redirect sensitive headers leak. Upgrade urllib3 to 2.7.0+ for fix. - "CVE-2026-44432", # urllib3: Decompression resource exhaustion in streaming API. Upgrade urllib3 to 2.7.0+ for fix. ] [tool.uv] diff --git a/scripts/hooks/run-pip-audit.sh b/scripts/hooks/run-pip-audit.sh index 750c0795..7bc4ca93 100755 --- a/scripts/hooks/run-pip-audit.sh +++ b/scripts/hooks/run-pip-audit.sh @@ -1,27 +1,113 @@ #!/bin/bash # Helper script to build pip-audit ignore arguments from pyproject.toml [tool.pip-audit] -# This ensures both pre-commit and CI use the same CVE exclusion list +# This ensures both pre-commit and CI use the same vulnerability exclusion list -# Extract ignore list from pyproject.toml using Python -IGNORE_ARGS=$(python3 << 'EOF' -import tomllib +set -euo pipefail + +TMP_AUDIT_JSON=$(mktemp) +trap 'rm -f "$TMP_AUDIT_JSON"' EXIT + +python3 - "$TMP_AUDIT_JSON" <<'EOF' +from __future__ import annotations + +import json +import subprocess import sys +import tomllib +from pathlib import Path + + +CONFIG_PATH = Path("pyproject.toml") +AUDIT_PATH = Path(sys.argv[1]) try: - with open("pyproject.toml", "rb") as f: - config = tomllib.load(f) + with CONFIG_PATH.open("rb") as config_file: + config = tomllib.load(config_file) +except Exception as exc: + print(f"Error reading {CONFIG_PATH}: {exc}", file=sys.stderr) + sys.exit(1) + +ignore_list = config.get("tool", {}).get("pip-audit", {}).get("ignore", []) + +if not ignore_list: + sys.exit(0) - # Get the ignore list from [tool.pip-audit] - ignore_list = config.get("tool", {}).get("pip-audit", {}).get("ignore", []) +result = subprocess.run( + [ + "uv", + "run", + "pip-audit", + "--format", + "json", + "--aliases", + "on", + "--progress-spinner", + "off", + "--output", + str(AUDIT_PATH), + ".", + ], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, +) + +if result.returncode not in (0, 1): + if result.stderr: + print(result.stderr, file=sys.stderr, end="") + sys.exit(result.returncode) - # Build the arguments - args = " ".join([f"--ignore-vuln {cve}" for cve in ignore_list]) - print(args) -except Exception as e: - print(f"Error reading pyproject.toml: {e}", file=sys.stderr) +try: + with AUDIT_PATH.open("r", encoding="utf-8") as audit_file: + audit_data = json.load(audit_file) +except Exception as exc: + print(f"Error reading pip-audit JSON output: {exc}", file=sys.stderr) sys.exit(1) + +active_ids: set[str] = set() +for dependency in audit_data.get("dependencies", []): + for vulnerability in dependency.get("vulns", []): + vuln_id = vulnerability.get("id") + if vuln_id: + active_ids.add(vuln_id) + for alias in vulnerability.get("aliases", []): + if alias: + active_ids.add(alias) + +stale_ignores = [vuln_id for vuln_id in ignore_list if vuln_id not in active_ids] + +if stale_ignores: + print( + "Stale pip-audit ignore entries in pyproject.toml: " + + ", ".join(stale_ignores), + file=sys.stderr, + ) + print( + "Remove ignore entries for vulnerabilities that are no longer reported.", + file=sys.stderr, + ) + sys.exit(1) +EOF + +IGNORE_ARGS=() +while IFS= read -r ignore_id; do + IGNORE_ARGS+=("--ignore-vuln" "$ignore_id") +done < <( + python3 <<'EOF' +import sys +import tomllib + +try: + with open("pyproject.toml", "rb") as config_file: + config = tomllib.load(config_file) +except Exception as exc: + print(f"Error reading pyproject.toml: {exc}", file=sys.stderr) + sys.exit(1) + +for ignore_id in config.get("tool", {}).get("pip-audit", {}).get("ignore", []): + print(ignore_id) EOF ) -# Run pip-audit with the exclusions from pyproject.toml -uv run pip-audit $IGNORE_ARGS "$@" +uv run pip-audit "${IGNORE_ARGS[@]}" "$@" From 544f616cb9866be19173a760f32c21f49265a4a6 Mon Sep 17 00:00:00 2001 From: Thom Ezechiels Date: Tue, 7 Jul 2026 11:41:37 +0200 Subject: [PATCH 159/199] Create Techletes Superpowers plugin --- .agents/plugins/marketplace.json | 20 + .agents/skills/caveman/README.md | 48 - .agents/skills/caveman/SKILL.md | 74 -- .../run-pip-audit.sh | 27 - .codex/hooks.json | 15 - .vscode/settings.json | 2 +- .../.codex-plugin/NOTICE.md | 6 + .../.codex-plugin/plugin.json | 52 + .../agents/coder.agent.md | 0 .../agents/designer.agent.md | 0 .../agents/orchestrator.agent.md | 0 .../agents/planner.agent.md | 0 .../techletes-superpowers/hooks/hooks.json | 57 + .../hooks/ponytail/ponytail-activate.js | 91 ++ .../hooks/ponytail/ponytail-config.js | 147 +++ .../hooks/ponytail/ponytail-instructions.js | 94 ++ .../hooks/ponytail/ponytail-mode-tracker.js | 83 ++ .../hooks/ponytail/ponytail-runtime.js | 68 + .../hooks/ponytail/ponytail-statusline.sh | 12 + .../hooks/ponytail/ponytail-subagent.js | 22 + .../skills/brainstorming/SKILL.md | 159 +++ .../brainstorming/scripts/frame-template.html | 213 +++ .../skills/brainstorming/scripts/helper.js | 167 +++ .../skills/brainstorming/scripts/server.cjs | 723 +++++++++++ .../brainstorming/scripts/start-server.sh | 209 +++ .../brainstorming/scripts/stop-server.sh | 120 ++ .../spec-document-reviewer-prompt.md | 49 + .../skills/brainstorming/visual-companion.md | 291 +++++ .../codeql-analysis-remediation/SKILL.md | 0 .../assets/codeql-commands.sh | 0 .../assets/report-template.md | 0 .../assets/task-delegate-template.md | 0 .../references/codeql-workflow.md | 0 .../references/safe-remediation-rules.md | 0 .../references/sarif-interpretation.md | 0 .../references/verification-commands.md | 0 .../dispatching-parallel-agents/SKILL.md | 185 +++ .../skills/executing-plans/SKILL.md | 70 + .../finishing-a-development-branch/SKILL.md | 241 ++++ .../skills/graphify/.graphify_version | 0 .../skills/graphify/SKILL.md | 0 .../skills/receiving-code-review/SKILL.md | 213 +++ .../skills/requesting-code-review/SKILL.md | 103 ++ .../requesting-code-review/code-reviewer.md | 172 +++ .../subagent-driven-development/SKILL.md | 418 ++++++ .../implementer-prompt.md | 139 ++ .../scripts/review-package | 44 + .../scripts/sdd-workspace | 22 + .../scripts/task-brief | 40 + .../task-reviewer-prompt.md | 188 +++ .../systematic-debugging/CREATION-LOG.md | 119 ++ .../skills/systematic-debugging/SKILL.md | 296 +++++ .../condition-based-waiting-example.ts | 158 +++ .../condition-based-waiting.md | 115 ++ .../systematic-debugging/defense-in-depth.md | 122 ++ .../systematic-debugging/find-polluter.sh | 63 + .../root-cause-tracing.md | 169 +++ .../systematic-debugging/test-academic.md | 14 + .../systematic-debugging/test-pressure-1.md | 58 + .../systematic-debugging/test-pressure-2.md | 68 + .../systematic-debugging/test-pressure-3.md | 69 + .../skills/test-driven-development/SKILL.md | 371 ++++++ .../testing-anti-patterns.md | 299 +++++ .../SKILL.md | 0 .../run-pip-audit.sh | 113 ++ .../skills/using-git-worktrees/SKILL.md | 202 +++ .../skills/using-superpowers/SKILL.md | 62 + .../references/antigravity-tools.md | 23 + .../references/codex-tools.md | 39 + .../using-superpowers/references/pi-tools.md | 16 + .../verification-before-completion/SKILL.md | 139 ++ .../skills/writing-plans/SKILL.md | 174 +++ .../plan-document-reviewer-prompt.md | 49 + .../skills/writing-skills/SKILL.md | 689 ++++++++++ .../anthropic-best-practices.md | 1150 +++++++++++++++++ .../examples/CLAUDE_MD_TESTING.md | 189 +++ .../writing-skills/graphviz-conventions.dot | 172 +++ .../writing-skills/persuasion-principles.md | 187 +++ .../skills/writing-skills/render-graphs.js | 168 +++ .../testing-skills-with-subagents.md | 384 ++++++ superpowers | 1 + 81 files changed, 10097 insertions(+), 165 deletions(-) create mode 100644 .agents/plugins/marketplace.json delete mode 100644 .agents/skills/caveman/README.md delete mode 100644 .agents/skills/caveman/SKILL.md delete mode 100644 .agents/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh delete mode 100644 .codex/hooks.json create mode 100644 plugins/techletes-superpowers/.codex-plugin/NOTICE.md create mode 100644 plugins/techletes-superpowers/.codex-plugin/plugin.json rename {.agents => plugins/techletes-superpowers}/agents/coder.agent.md (100%) rename {.agents => plugins/techletes-superpowers}/agents/designer.agent.md (100%) rename {.agents => plugins/techletes-superpowers}/agents/orchestrator.agent.md (100%) rename {.agents => plugins/techletes-superpowers}/agents/planner.agent.md (100%) create mode 100644 plugins/techletes-superpowers/hooks/hooks.json create mode 100644 plugins/techletes-superpowers/hooks/ponytail/ponytail-activate.js create mode 100644 plugins/techletes-superpowers/hooks/ponytail/ponytail-config.js create mode 100644 plugins/techletes-superpowers/hooks/ponytail/ponytail-instructions.js create mode 100644 plugins/techletes-superpowers/hooks/ponytail/ponytail-mode-tracker.js create mode 100644 plugins/techletes-superpowers/hooks/ponytail/ponytail-runtime.js create mode 100755 plugins/techletes-superpowers/hooks/ponytail/ponytail-statusline.sh create mode 100644 plugins/techletes-superpowers/hooks/ponytail/ponytail-subagent.js create mode 100644 plugins/techletes-superpowers/skills/brainstorming/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/brainstorming/scripts/frame-template.html create mode 100644 plugins/techletes-superpowers/skills/brainstorming/scripts/helper.js create mode 100644 plugins/techletes-superpowers/skills/brainstorming/scripts/server.cjs create mode 100755 plugins/techletes-superpowers/skills/brainstorming/scripts/start-server.sh create mode 100755 plugins/techletes-superpowers/skills/brainstorming/scripts/stop-server.sh create mode 100644 plugins/techletes-superpowers/skills/brainstorming/spec-document-reviewer-prompt.md create mode 100644 plugins/techletes-superpowers/skills/brainstorming/visual-companion.md rename {.agents => plugins/techletes-superpowers}/skills/codeql-analysis-remediation/SKILL.md (100%) rename {.agents => plugins/techletes-superpowers}/skills/codeql-analysis-remediation/assets/codeql-commands.sh (100%) rename {.agents => plugins/techletes-superpowers}/skills/codeql-analysis-remediation/assets/report-template.md (100%) rename {.agents => plugins/techletes-superpowers}/skills/codeql-analysis-remediation/assets/task-delegate-template.md (100%) rename {.agents => plugins/techletes-superpowers}/skills/codeql-analysis-remediation/references/codeql-workflow.md (100%) rename {.agents => plugins/techletes-superpowers}/skills/codeql-analysis-remediation/references/safe-remediation-rules.md (100%) rename {.agents => plugins/techletes-superpowers}/skills/codeql-analysis-remediation/references/sarif-interpretation.md (100%) rename {.agents => plugins/techletes-superpowers}/skills/codeql-analysis-remediation/references/verification-commands.md (100%) create mode 100644 plugins/techletes-superpowers/skills/dispatching-parallel-agents/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/executing-plans/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/finishing-a-development-branch/SKILL.md rename {.agents => plugins/techletes-superpowers}/skills/graphify/.graphify_version (100%) rename {.agents => plugins/techletes-superpowers}/skills/graphify/SKILL.md (100%) create mode 100644 plugins/techletes-superpowers/skills/receiving-code-review/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/requesting-code-review/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/requesting-code-review/code-reviewer.md create mode 100644 plugins/techletes-superpowers/skills/subagent-driven-development/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/subagent-driven-development/implementer-prompt.md create mode 100755 plugins/techletes-superpowers/skills/subagent-driven-development/scripts/review-package create mode 100755 plugins/techletes-superpowers/skills/subagent-driven-development/scripts/sdd-workspace create mode 100755 plugins/techletes-superpowers/skills/subagent-driven-development/scripts/task-brief create mode 100644 plugins/techletes-superpowers/skills/subagent-driven-development/task-reviewer-prompt.md create mode 100644 plugins/techletes-superpowers/skills/systematic-debugging/CREATION-LOG.md create mode 100644 plugins/techletes-superpowers/skills/systematic-debugging/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/systematic-debugging/condition-based-waiting-example.ts create mode 100644 plugins/techletes-superpowers/skills/systematic-debugging/condition-based-waiting.md create mode 100644 plugins/techletes-superpowers/skills/systematic-debugging/defense-in-depth.md create mode 100755 plugins/techletes-superpowers/skills/systematic-debugging/find-polluter.sh create mode 100644 plugins/techletes-superpowers/skills/systematic-debugging/root-cause-tracing.md create mode 100644 plugins/techletes-superpowers/skills/systematic-debugging/test-academic.md create mode 100644 plugins/techletes-superpowers/skills/systematic-debugging/test-pressure-1.md create mode 100644 plugins/techletes-superpowers/skills/systematic-debugging/test-pressure-2.md create mode 100644 plugins/techletes-superpowers/skills/systematic-debugging/test-pressure-3.md create mode 100644 plugins/techletes-superpowers/skills/test-driven-development/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/test-driven-development/testing-anti-patterns.md rename {.agents => plugins/techletes-superpowers}/skills/update-dependencies-fix-vulnerabilities/SKILL.md (100%) create mode 100755 plugins/techletes-superpowers/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh create mode 100644 plugins/techletes-superpowers/skills/using-git-worktrees/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/using-superpowers/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/using-superpowers/references/antigravity-tools.md create mode 100644 plugins/techletes-superpowers/skills/using-superpowers/references/codex-tools.md create mode 100644 plugins/techletes-superpowers/skills/using-superpowers/references/pi-tools.md create mode 100644 plugins/techletes-superpowers/skills/verification-before-completion/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/writing-plans/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/writing-plans/plan-document-reviewer-prompt.md create mode 100644 plugins/techletes-superpowers/skills/writing-skills/SKILL.md create mode 100644 plugins/techletes-superpowers/skills/writing-skills/anthropic-best-practices.md create mode 100644 plugins/techletes-superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md create mode 100644 plugins/techletes-superpowers/skills/writing-skills/graphviz-conventions.dot create mode 100644 plugins/techletes-superpowers/skills/writing-skills/persuasion-principles.md create mode 100755 plugins/techletes-superpowers/skills/writing-skills/render-graphs.js create mode 100644 plugins/techletes-superpowers/skills/writing-skills/testing-skills-with-subagents.md create mode 160000 superpowers diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 00000000..3977f3d9 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "interface": { + "displayName": "Techletes Plugins" + }, + "name": "techletes-plugins", + "plugins": [ + { + "category": "Developer Tools", + "name": "techletes-superpowers", + "policy": { + "authentication": "ON_INSTALL", + "installation": "AVAILABLE" + }, + "source": { + "path": "./plugins/techletes-superpowers", + "source": "local" + } + } + ] +} diff --git a/.agents/skills/caveman/README.md b/.agents/skills/caveman/README.md deleted file mode 100644 index d749b834..00000000 --- a/.agents/skills/caveman/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# caveman - -Talk like smart caveman. Same brain, fewer tokens. - -## What it does - -Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped. - -Six intensity levels: - -| Level | What change | -|-------|-------------| -| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. | -| `full` | Default. Drop articles, fragments OK, short synonyms. | -| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. | -| `wenyan-lite` | Classical Chinese register, light compression. | -| `wenyan-full` | Maximum ๆ–‡่จ€ๆ–‡. 80-90% character reduction. | -| `wenyan-ultra` | Extreme classical compression. | - -Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part. - -## How to invoke - -``` -/caveman # full mode (default) -/caveman lite # lighter compression -/caveman ultra # extreme compression -/caveman wenyan # classical Chinese -stop caveman # back to normal prose -``` - -## Example output - -Question: "Why does my React component re-render?" - -Normal prose: -> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue. - -Caveman (full): -> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`. - -Caveman (ultra): -> Inline obj prop โ†’ new ref โ†’ re-render. `useMemo`. - -## See also - -- [`SKILL.md`](./SKILL.md) โ€” full LLM-facing instructions -- [Caveman README](../../README.md) โ€” repo overview, install, benchmarks diff --git a/.agents/skills/caveman/SKILL.md b/.agents/skills/caveman/SKILL.md deleted file mode 100644 index 09ea3ef2..00000000 --- a/.agents/skills/caveman/SKILL.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -name: caveman -description: > - Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman - while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, - wenyan-lite, wenyan-full, wenyan-ultra. - Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", - "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested. ---- - -Respond terse like smart caveman. All technical substance stay. Only fluff die. - -## Persistence - -ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode". - -Default: **full**. Switch: `/caveman lite|full|ultra`. - -## Rules - -Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact. - -Pattern: `[thing] [action] [reason]. [next step].` - -Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." -Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" - -## Intensity - -| Level | What change | -|-------|------------| -| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight | -| **full** | Drop articles, fragments OK, short synonyms. Classic caveman | -| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X โ†’ Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate | -| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register | -| **wenyan-full** | Maximum classical terseness. Fully ๆ–‡่จ€ๆ–‡. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (ไน‹/ไนƒ/็‚บ/ๅ…ถ) | -| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse | - -Example โ€” "Why React component re-render?" -- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`." -- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`." -- ultra: "Inline obj prop โ†’ new ref โ†’ re-render. `useMemo`." -- wenyan-lite: "็ต„ไปถ้ ป้‡็นช๏ผŒไปฅๆฏ็นชๆ–ฐ็”Ÿๅฐ่ฑกๅƒ็…งๆ•…ใ€‚ไปฅ useMemo ๅŒ…ไน‹ใ€‚" -- wenyan-full: "็‰ฉๅ‡บๆ–ฐๅƒ็…ง๏ผŒ่‡ด้‡็นชใ€‚useMemo .Wrapไน‹ใ€‚" -- wenyan-ultra: "ๆ–ฐๅƒ็…งโ†’้‡็นชใ€‚useMemo Wrapใ€‚" - -Example โ€” "Explain database connection pooling." -- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead." -- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead." -- ultra: "Pool = reuse DB conn. Skip handshake โ†’ fast under load." -- wenyan-full: "ๆฑ reuse open connectionใ€‚ไธๆฏreqๆ–ฐ้–‹ใ€‚skip handshake overheadใ€‚" -- wenyan-ultra: "ๆฑ reuse connใ€‚skip handshake โ†’ fastใ€‚" - -## Auto-Clarity - -Drop caveman when: -- Security warnings -- Irreversible action confirmations -- Multi-step sequences where fragment order or omitted conjunctions risk misread -- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` โ€” order unclear without articles/conjunctions) -- User asks to clarify or repeats question - -Resume caveman after clear part done. - -Example โ€” destructive op: -> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. -> ```sql -> DROP TABLE users; -> ``` -> Caveman resume. Verify backup exist first. - -## Boundaries - -Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end. diff --git a/.agents/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh b/.agents/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh deleted file mode 100644 index 0202b037..00000000 --- a/.agents/skills/update-dependencies-fix-vulnerabilities/run-pip-audit.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# Helper script to build pip-audit ignore arguments from pyproject.toml [tool.pip-audit] -# This ensures both pre-commit and CI use the same CVE exclusion list - -# Extract ignore list from pyproject.toml using Python -IGNORE_ARGS=$(python3 << 'EOF' -import tomllib -import sys - -try: - with open("pyproject.toml", "rb") as f: - config = tomllib.load(f) - - # Get the ignore list from [tool.pip-audit] - ignore_list = config.get("tool", {}).get("pip-audit", {}).get("ignore", []) - - # Build the arguments - args = " ".join([f"--ignore-vuln {cve}" for cve in ignore_list]) - print(args) -except Exception as e: - print(f"Error reading pyproject.toml: {e}", file=sys.stderr) - sys.exit(1) -EOF -) - -# Run pip-audit with the exclusions from pyproject.toml -pip-audit $IGNORE_ARGS "$@" diff --git a/.codex/hooks.json b/.codex/hooks.json deleted file mode 100644 index 921623cf..00000000 --- a/.codex/hooks.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "hooks": { - "PreToolUse": [ - { - "hooks": [ - { - "command": "uv run graphify hook-check", - "type": "command" - } - ], - "matcher": "Bash" - } - ] - } -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 57dc60e0..ddb6d07a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,7 @@ "editor.formatOnSave": true }, "[json]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.defaultFormatter": "vscode.json-language-features", "editor.formatOnSave": true }, "[python]": { diff --git a/plugins/techletes-superpowers/.codex-plugin/NOTICE.md b/plugins/techletes-superpowers/.codex-plugin/NOTICE.md new file mode 100644 index 00000000..bf3837c2 --- /dev/null +++ b/plugins/techletes-superpowers/.codex-plugin/NOTICE.md @@ -0,0 +1,6 @@ +# Notice + +This plugin is inspired by and/or adapted from Superpowers by Jesse Vincent: +https://github.com/obra/superpowers + +Original project license: MIT. diff --git a/plugins/techletes-superpowers/.codex-plugin/plugin.json b/plugins/techletes-superpowers/.codex-plugin/plugin.json new file mode 100644 index 00000000..32c86b73 --- /dev/null +++ b/plugins/techletes-superpowers/.codex-plugin/plugin.json @@ -0,0 +1,52 @@ +{ + "author": { + "email": "thom@techletes.ai", + "name": "Techletes", + "url": "https://techletes.ai" + }, + "description": "Techletes' agentic software development workflow framework: lean planning, pragmatic implementation, TDD where useful, debugging, review, and delivery workflows for coding agents.", + "homepage": "https://github.com/TECHLETES/python_template", + "hooks": {}, + "interface": { + "brandColor": "#111827", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "category": "Developer Tools", + "composerIcon": "./assets/techletes-superpowers-small.svg", + "defaultPrompt": [ + "Help me plan and implement this feature using the Techletes workflow.", + "Review this issue and propose the smallest useful implementation path.", + "Debug this failing test or bug systematically.", + "Finish this branch and prepare it for review." + ], + "developerName": "Techletes", + "displayName": "Techletes Superpowers", + "logo": "./assets/app-icon.png", + "longDescription": "Use Techletes Superpowers to guide coding agents through practical software delivery workflows: deciding what not to build, planning only when useful, implementing minimal working changes, using tests where they add value, debugging systematically, reviewing changes, and finishing branches cleanly.", + "privacyPolicyURL": "https://techletes.ai/privacy", + "screenshots": [], + "shortDescription": "Lean planning, implementation, debugging, review, and delivery workflows for coding agents", + "termsOfServiceURL": "https://techletes.ai/terms", + "websiteURL": "https://github.com/TECHLETES/python_template" + }, + "keywords": [ + "agent-workflows", + "skills", + "planning", + "lean-development", + "tdd", + "debugging", + "code-review", + "delivery", + "python", + "techletes" + ], + "license": "MIT", + "name": "techletes-superpowers", + "repository": "https://github.com/TECHLETES/python_template", + "skills": "./skills/", + "version": "0.1.0" +} diff --git a/.agents/agents/coder.agent.md b/plugins/techletes-superpowers/agents/coder.agent.md similarity index 100% rename from .agents/agents/coder.agent.md rename to plugins/techletes-superpowers/agents/coder.agent.md diff --git a/.agents/agents/designer.agent.md b/plugins/techletes-superpowers/agents/designer.agent.md similarity index 100% rename from .agents/agents/designer.agent.md rename to plugins/techletes-superpowers/agents/designer.agent.md diff --git a/.agents/agents/orchestrator.agent.md b/plugins/techletes-superpowers/agents/orchestrator.agent.md similarity index 100% rename from .agents/agents/orchestrator.agent.md rename to plugins/techletes-superpowers/agents/orchestrator.agent.md diff --git a/.agents/agents/planner.agent.md b/plugins/techletes-superpowers/agents/planner.agent.md similarity index 100% rename from .agents/agents/planner.agent.md rename to plugins/techletes-superpowers/agents/planner.agent.md diff --git a/plugins/techletes-superpowers/hooks/hooks.json b/plugins/techletes-superpowers/hooks/hooks.json new file mode 100644 index 00000000..d11e43a7 --- /dev/null +++ b/plugins/techletes-superpowers/hooks/hooks.json @@ -0,0 +1,57 @@ +{ + "hooks": { + "PreToolUse": [ + { + "hooks": [ + { + "command": "uv run graphify hook-check", + "statusMessage": "Checking command...", + "timeout": 5, + "type": "command" + } + ], + "matcher": "Bash" + } + ], + "SessionStart": [ + { + "hooks": [ + { + "command": "node \"${PLUGIN_ROOT}/hooks/ponytail/ponytail-activate.js\"", + "commandWindows": "if (Get-Command node -ErrorAction SilentlyContinue) { node \"$env:PLUGIN_ROOT\\hooks\\ponytail\\ponytail-activate.js\" }", + "statusMessage": "Loading Techletes mode...", + "timeout": 5, + "type": "command" + } + ], + "matcher": "startup|resume|clear|compact" + } + ], + "SubagentStart": [ + { + "hooks": [ + { + "command": "node \"${PLUGIN_ROOT}/hooks/ponytail/ponytail-subagent.js\"", + "commandWindows": "if (Get-Command node -ErrorAction SilentlyContinue) { node \"$env:PLUGIN_ROOT\\hooks\\ponytail\\ponytail-subagent.js\" }", + "statusMessage": "Loading Techletes mode...", + "timeout": 5, + "type": "command" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "command": "node \"${PLUGIN_ROOT}/hooks/ponytail/ponytail-mode-tracker.js\"", + "commandWindows": "if (Get-Command node -ErrorAction SilentlyContinue) { node \"$env:PLUGIN_ROOT\\hooks\\ponytail\\ponytail-mode-tracker.js\" }", + "statusMessage": "Tracking Techletes mode...", + "timeout": 5, + "type": "command" + } + ] + } + ] + } +} diff --git a/plugins/techletes-superpowers/hooks/ponytail/ponytail-activate.js b/plugins/techletes-superpowers/hooks/ponytail/ponytail-activate.js new file mode 100644 index 00000000..3087c7c1 --- /dev/null +++ b/plugins/techletes-superpowers/hooks/ponytail/ponytail-activate.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node +// ponytail โ€” Claude Code SessionStart activation hook +// +// Runs on every session start: +// 1. Writes flag file at $CLAUDE_CONFIG_DIR/.ponytail-active (defaults to ~/.claude; statusline reads this) +// 2. Emits ponytail ruleset as hidden SessionStart context +// 3. Detects missing statusline config and emits setup nudge + +const fs = require('fs'); +const path = require('path'); +const { getDefaultMode, getClaudeDir, isShellSafe } = require('./ponytail-config'); +const { getPonytailInstructions } = require('./ponytail-instructions'); +const { + clearMode, + isCodex, + isCopilot, + setMode, + writeHookOutput, +} = require('./ponytail-runtime'); + +const claudeDir = getClaudeDir(); +const settingsPath = path.join(claudeDir, 'settings.json'); + +const mode = getDefaultMode(); + +// "off" mode โ€” skip activation entirely, don't write flag or emit rules +if (mode === 'off') { + clearMode(); + const hookOutput = (isCodex || isCopilot) ? '' : 'OK'; + writeHookOutput('SessionStart', 'off', hookOutput); + process.exit(0); +} + +// 1. Write flag file +try { + setMode(mode); +} catch (e) { + // Silent fail -- flag is best-effort, don't block the hook +} + +// 2. Emit the ponytail ruleset, filtered to the active intensity level. +let output = getPonytailInstructions(mode); + +// 3. Detect missing statusline config โ€” nudge Claude to help set it up +if (!isCodex && !isCopilot) try { + let hasStatusline = false; + if (fs.existsSync(settingsPath)) { + // Strip UTF-8 BOM some editors prepend on Windows (breaks JSON.parse) + const raw = fs.readFileSync(settingsPath, 'utf8').replace(/^\uFEFF/, ''); + const settings = JSON.parse(raw); + if (settings.statusLine) { + hasStatusline = true; + } + } + + if (!hasStatusline) { + const isWindows = process.platform === 'win32'; + const scriptName = isWindows ? 'ponytail-statusline.ps1' : 'ponytail-statusline.sh'; + const scriptPath = path.join(__dirname, scriptName); + if (isShellSafe(scriptPath)) { + const command = isWindows + ? `powershell -ExecutionPolicy Bypass -File "${scriptPath}"` + : `bash "${scriptPath}"`; + const statusLineSnippet = + '"statusLine": { "type": "command", "command": ' + JSON.stringify(command) + ' }'; + output += "\n\n" + + "STATUSLINE SETUP NEEDED: The ponytail plugin includes a statusline badge showing active mode " + + "(e.g. [PONYTAIL], [PONYTAIL:ULTRA]). It is not configured yet. " + + "To enable, add this to " + settingsPath + ": " + + statusLineSnippet + " " + + "Proactively offer to set this up for the user on first interaction."; + } else { + // ponytail: install path has shell metacharacters โ€” don't embed it in a + // command snippet; have the agent wire it up by hand instead. + output += "\n\n" + + "STATUSLINE SETUP NEEDED: The ponytail plugin includes a statusline badge showing active mode. " + + "Its install path contains characters unsafe to embed in a shell command, so configure it manually: " + + "add a statusLine command of type \"command\" that runs " + scriptName + + " from the plugin's hooks directory to " + settingsPath + ", quoting/escaping the path for your shell. " + + "Proactively offer to set this up for the user on first interaction."; + } + } +} catch (e) { + // Silent fail โ€” don't block session start over statusline detection +} + +try { + writeHookOutput('SessionStart', mode, output); +} catch (e) { + // Silent fail โ€” stdout closed/EPIPE at hook exit must not surface as a hook failure +} diff --git a/plugins/techletes-superpowers/hooks/ponytail/ponytail-config.js b/plugins/techletes-superpowers/hooks/ponytail/ponytail-config.js new file mode 100644 index 00000000..4233c2b2 --- /dev/null +++ b/plugins/techletes-superpowers/hooks/ponytail/ponytail-config.js @@ -0,0 +1,147 @@ +#!/usr/bin/env node +// ponytail โ€” shared configuration resolver +// +// Resolution order for default mode: +// 1. PONYTAIL_DEFAULT_MODE environment variable +// 2. Config file defaultMode field: +// - $XDG_CONFIG_HOME/ponytail/config.json (any platform, if set) +// - ~/.config/ponytail/config.json (macOS / Linux fallback) +// - %APPDATA%\ponytail\config.json (Windows fallback) +// 3. 'full' + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const DEFAULT_MODE = 'full'; +const VALID_MODES = ['off', 'lite', 'full', 'ultra', 'review']; +const RUNTIME_MODES = ['off', 'lite', 'full', 'ultra']; + +function normalizeMode(mode) { + if (typeof mode !== 'string') return null; + const normalized = mode.trim().toLowerCase(); + return RUNTIME_MODES.includes(normalized) ? normalized : null; +} + +function normalizeConfigMode(mode) { + if (typeof mode !== 'string') return null; + const normalized = mode.trim().toLowerCase(); + return VALID_MODES.includes(normalized) ? normalized : null; +} + +function normalizePersistedMode(mode) { + return normalizeMode(mode) || normalizeConfigMode(mode); +} + +// "stop ponytail" / "normal mode" turn ponytail off, but only as a standalone +// command. Matching the phrase anywhere in the message turned it off mid-task +// for ordinary requests like "add a normal mode toggle" โ€” so require the whole +// message to be the command, ignoring case and trailing punctuation. +function isDeactivationCommand(text) { + const t = String(text || '').trim().toLowerCase().replace(/[.!?\s]+$/, ''); + return t === 'stop ponytail' || t === 'normal mode'; +} + +// ponytail: only embed the plugin install path in a statusline shell command when +// it's made of ordinary path characters. An allowlist beats escaping every shell's +// metacharacters; a hostile clone path (quotes, &, $, backtick, ;, etc.) falls back +// to manual setup instead. Allows : \ / for normal Windows and POSIX paths. Full +// per-shell escaper only if a real need appears. +function isShellSafe(p) { + return typeof p === 'string' && /^[A-Za-z0-9 _.\-:/\\~]+$/.test(p); +} + +function getConfigDir() { + if (process.env.XDG_CONFIG_HOME) { + return path.join(process.env.XDG_CONFIG_HOME, 'ponytail'); + } + if (process.platform === 'win32') { + return path.join( + process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), + 'ponytail' + ); + } + return path.join(os.homedir(), '.config', 'ponytail'); +} + +function getConfigPath() { + return path.join(getConfigDir(), 'config.json'); +} + +function getClaudeDir() { + // ponytail: CLAUDE_CONFIG_DIR overrides ~/.claude, matching Claude Code. + return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'); +} + +function getDefaultMode() { + // 1. Environment variable (highest priority) + const envMode = process.env.PONYTAIL_DEFAULT_MODE; + if (envMode && VALID_MODES.includes(envMode.toLowerCase())) { + return envMode.toLowerCase(); + } + + // 2. Config file + try { + const configPath = getConfigPath(); + // Strip UTF-8 BOM (common on Windows-saved files) so JSON.parse doesn't choke + const config = JSON.parse(fs.readFileSync(configPath, 'utf8').replace(/^\uFEFF/, '')); + if (config.defaultMode && VALID_MODES.includes(config.defaultMode.toLowerCase())) { + return config.defaultMode.toLowerCase(); + } + } catch (e) { + // Config file doesn't exist or is invalid โ€” fall through + } + + // 3. Default + return DEFAULT_MODE; +} + +// Hide the status-bar indicator while keeping ponytail active (#324). +// PONYTAIL_HIDE_STATUS=1 (or any truthy value; 0/false/empty mean "don't hide") +// takes precedence, else config.hideStatus === true. +function getHideStatus() { + const env = process.env.PONYTAIL_HIDE_STATUS; + if (env !== undefined) { + const v = env.trim().toLowerCase(); + return v !== '' && v !== '0' && v !== 'false' && v !== 'no'; + } + try { + const config = JSON.parse(fs.readFileSync(getConfigPath(), 'utf8').replace(/^\uFEFF/, '')); + return config.hideStatus === true; + } catch (_) { + return false; + } +} + +function writeDefaultMode(mode) { + const normalized = normalizeConfigMode(mode); + if (!normalized) return null; + + const configPath = getConfigPath(); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + let config = {}; + try { + config = JSON.parse(fs.readFileSync(configPath, 'utf8').replace(/^\uFEFF/, '')); + if (!config || typeof config !== 'object' || Array.isArray(config)) config = {}; + } catch (_) {} + config.defaultMode = normalized; + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8'); + return normalized; +} + +module.exports = { + DEFAULT_MODE, + VALID_MODES, + RUNTIME_MODES, + getDefaultMode, + getConfigDir, + getConfigPath, + getClaudeDir, + getHideStatus, + isShellSafe, + normalizeMode, + normalizeConfigMode, + normalizePersistedMode, + isDeactivationCommand, + writeDefaultMode, +}; diff --git a/plugins/techletes-superpowers/hooks/ponytail/ponytail-instructions.js b/plugins/techletes-superpowers/hooks/ponytail/ponytail-instructions.js new file mode 100644 index 00000000..a516b757 --- /dev/null +++ b/plugins/techletes-superpowers/hooks/ponytail/ponytail-instructions.js @@ -0,0 +1,94 @@ +#!/usr/bin/env node +// Shared Ponytail instruction builder for Claude hooks and Pi extension. + +const fs = require('fs'); +const path = require('path'); +const { DEFAULT_MODE, normalizeMode, normalizePersistedMode } = require('./ponytail-config'); + +const INDEPENDENT_MODES = new Set(['review']); +const SKILL_PATH = path.join(__dirname, '..', 'skills', 'ponytail', 'SKILL.md'); + +function filterSkillBodyForMode(body, mode) { + const effectiveMode = normalizeMode(mode) || DEFAULT_MODE; + const withoutFrontmatter = String(body || '').replace(/^---[\s\S]*?---\s*/, ''); + + // Only the intensity table rows and worked examples are mode-specific, and + // both are keyed by a mode name (lite/full/ultra). A bullet whose label is + // not a mode โ€” e.g. "No unrequested abstractions: ..." โ€” is a normal rule + // and must be kept verbatim. + return withoutFrontmatter + .split(/\r?\n/) + .filter((line) => { + const tableLabel = line.match(/^\|\s*\*\*(.+?)\*\*\s*\|/); + if (tableLabel) { + const labelMode = normalizeMode(tableLabel[1].trim()); + if (labelMode) return labelMode === effectiveMode; + } + + const exampleLabel = line.match(/^-\s*([^:]+):\s*/); + if (exampleLabel) { + const labelMode = normalizeMode(exampleLabel[1].trim()); + if (labelMode) return labelMode === effectiveMode; + } + + return true; + }) + .join('\n'); +} + +function getFallbackInstructions(mode) { + return 'PONYTAIL MODE ACTIVE โ€” level: ' + mode + '\n\n' + + 'You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.\n\n' + + '## Persistence\n\n' + + 'ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure. Off only: "stop ponytail" / "normal mode".\n\n' + + 'Current level: **' + mode + '**. Switch: `/ponytail lite|full|ultra`.\n\n' + + '## The ladder\n\n' + + 'Before any code, stop at the first rung that holds (the ladder runs after you understand the problem, not instead of it โ€” read the code it touches and trace the real flow first):\n' + + '1. Does this need to be built at all? (YAGNI)\n' + + '2. Does it already exist in this codebase? Reuse what is already here, do not re-write it.\n' + + '3. Does the standard library do this? Use it.\n' + + '4. Does a native platform feature cover it? Use it.\n' + + '5. Does an already-installed dependency solve it? Use it.\n' + + '6. Can this be one line? Make it one line.\n' + + '7. Only then: write the minimum code that works.\n\n' + + 'Bug fix = root cause, not symptom: grep every caller of the function you touch and fix the shared function once (a smaller diff than one guard per caller); patching only the path the ticket names leaves a sibling caller broken.\n\n' + + '## Rules\n\n' + + 'No abstractions that were not requested. No avoidable dependencies. No boilerplate nobody asked for. ' + + 'Deletion over addition. Boring over clever. Fewest files possible. ' + + 'Ship the lazy version and question the complex request in the same response โ€” never stall. ' + + 'Between two same-size stdlib options, pick the one correct on edge cases. ' + + 'Mark intentional simplifications with a `ponytail:` comment โ€” a shortcut with a known ceiling names the ceiling and the upgrade path in the comment.\n\n' + + '## Output\n\n' + + 'Code first. Then at most three short lines: what was skipped, when to add it. ' + + 'If the explanation is longer than the code, delete the explanation. ' + + 'Explanation the user explicitly asked for is not debt, give it in full.\n\n' + + '## When NOT to be lazy\n\n' + + 'Never simplify away: understanding the problem (read it fully and trace the real flow before picking a rung โ€” a small diff you do not understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, ' + + 'security measures, accessibility basics, the calibration real hardware needs (the platform is never the spec ideal), anything the user explicitly asked to keep. ' + + 'Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind (assert-based demo/self-check or one small test file; no frameworks). Trivial one-liners need no test.\n\n' + + '## Boundaries\n\n' + + 'Ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode": revert. Level persists until changed or session end.'; +} + +function getPonytailInstructions(mode) { + const configuredMode = normalizePersistedMode(mode) || DEFAULT_MODE; + + if (INDEPENDENT_MODES.has(configuredMode)) { + return 'PONYTAIL MODE ACTIVE โ€” level: ' + configuredMode + '. Behavior defined by /ponytail-' + configuredMode + ' skill.'; + } + + const effectiveMode = normalizeMode(configuredMode) || DEFAULT_MODE; + + try { + return 'PONYTAIL MODE ACTIVE โ€” level: ' + effectiveMode + '\n\n' + + filterSkillBodyForMode(fs.readFileSync(SKILL_PATH, 'utf8'), effectiveMode); + } catch (e) { + return getFallbackInstructions(effectiveMode); + } +} + +module.exports = { + filterSkillBodyForMode, + getFallbackInstructions, + getPonytailInstructions, +}; diff --git a/plugins/techletes-superpowers/hooks/ponytail/ponytail-mode-tracker.js b/plugins/techletes-superpowers/hooks/ponytail/ponytail-mode-tracker.js new file mode 100644 index 00000000..5202a746 --- /dev/null +++ b/plugins/techletes-superpowers/hooks/ponytail/ponytail-mode-tracker.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node +// ponytail โ€” UserPromptSubmit hook to track which ponytail mode is active +// Inspects user input for /ponytail commands and writes mode to flag file + +const { getDefaultMode, isDeactivationCommand, writeDefaultMode } = require('./ponytail-config'); +const { clearMode, setMode, writeHookOutput } = require('./ponytail-runtime'); + +let input = ''; +let done = false; + +function finish() { + if (done) return; + done = true; + try { + // Strip UTF-8 BOM some shells prepend when piping (breaks JSON.parse) + const data = JSON.parse(input.replace(/^\uFEFF/, '')); + const prompt = (data.prompt || '').trim().toLowerCase(); + + // Match /ponytail commands + if (/^[/@$]ponytail/.test(prompt)) { + const parts = prompt.split(/\s+/); + const cmd = parts[0].replace(/^[@$]/, '/'); + const arg = parts[1] || ''; + + let mode = null; + + if (cmd === '/ponytail-review' || cmd === '/ponytail:ponytail-review') { + mode = 'review'; + } else if (cmd === '/ponytail' || cmd === '/ponytail:ponytail') { + // `/ponytail default ` persists the default to config (survives + // restarts). Plain switches stay session-scoped ("sticks until session + // end"), so this is the only path that writes config. review is not a + // valid default (#377), so only off/lite/full/ultra are accepted. + if (arg === 'default') { + const dmode = parts[2]; + if (dmode === 'off' || dmode === 'lite' || dmode === 'full' || dmode === 'ultra') { + writeDefaultMode(dmode); + writeHookOutput('UserPromptSubmit', dmode, 'PONYTAIL DEFAULT SET โ€” new sessions start in ' + dmode + '.'); + } + return; // don't fall through to the session-mode switch + } + if (arg === 'lite') mode = 'lite'; + else if (arg === 'full') mode = 'full'; + else if (arg === 'ultra') mode = 'ultra'; + else if (arg === 'off') mode = 'off'; + else mode = getDefaultMode(); + } + + if (mode && mode !== 'off') { + setMode(mode); + writeHookOutput( + 'UserPromptSubmit', + mode, + 'PONYTAIL MODE CHANGED โ€” level: ' + mode, + ); + } else if (mode === 'off') { + clearMode(); + writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF'); + } + } + + // Detect deactivation + if (isDeactivationCommand(prompt)) { + clearMode(); + writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF'); + } + } catch (e) { + // Silent fail + } +} + +process.stdin.on('data', chunk => { input += chunk; }); +process.stdin.on('end', finish); + +// Never hang the session. On Windows, Claude Code runs this hook through a +// PowerShell `if {}` wrapper that can swallow the piped prompt JSON, so stdin +// 'end' never fires and the hook blocks forever โ€” freezing the session (#443). +// On error, or after a short fallback, process whatever arrived (recovering the +// mode if data came without EOF) and exit. unref() keeps the timer from adding +// latency to the normal path, where 'end' fires first. Mirrors the best-effort, +// never-block contract the other lifecycle hooks already follow. +process.stdin.on('error', () => { finish(); process.exit(0); }); +setTimeout(() => { finish(); process.exit(0); }, 1000).unref(); diff --git a/plugins/techletes-superpowers/hooks/ponytail/ponytail-runtime.js b/plugins/techletes-superpowers/hooks/ponytail/ponytail-runtime.js new file mode 100644 index 00000000..2aa61a31 --- /dev/null +++ b/plugins/techletes-superpowers/hooks/ponytail/ponytail-runtime.js @@ -0,0 +1,68 @@ +const fs = require('fs'); +const path = require('path'); +const { getClaudeDir } = require('./ponytail-config'); + +const STATE_FILE = '.ponytail-active'; +const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA); +const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA); + +let stateDir = getClaudeDir(); +if (isCodex) stateDir = process.env.PLUGIN_DATA; +if (isCopilot) stateDir = process.env.COPILOT_PLUGIN_DATA; + +const statePath = path.join(stateDir, STATE_FILE); + +function setMode(mode) { + fs.mkdirSync(path.dirname(statePath), { recursive: true }); + fs.writeFileSync(statePath, mode); +} + +function clearMode() { + try { fs.unlinkSync(statePath); } catch (e) {} +} + +// Live mode written by activate/mode-tracker. Absent flag = ponytail off. +function readMode() { + try { + return fs.readFileSync(statePath, 'utf8').trim() || null; + } catch (e) { + return null; + } +} + +function writeHookOutput(event, mode, context = '') { + if (isCopilot) { + // Copilot reads additionalContext on SessionStart; ignores output elsewhere. + process.stdout.write(JSON.stringify( + event === 'SessionStart' && context ? { additionalContext: context } : {})); + return; + } + if (isCodex) { + const output = { systemMessage: `PONYTAIL:${mode.toUpperCase()}` }; + if (context) { + output.hookSpecificOutput = { + hookEventName: event, + additionalContext: context, + }; + } + process.stdout.write(JSON.stringify(output)); + return; + } + // Native Claude: SessionStart accepts raw stdout, but SubagentStart needs the + // hookSpecificOutput JSON form or the context is dropped. + if (event === 'SubagentStart') { + process.stdout.write(JSON.stringify( + { hookSpecificOutput: { hookEventName: event, additionalContext: context } })); + return; + } + process.stdout.write(context); +} + +module.exports = { + clearMode, + isCodex, + isCopilot, + readMode, + setMode, + writeHookOutput, +}; diff --git a/plugins/techletes-superpowers/hooks/ponytail/ponytail-statusline.sh b/plugins/techletes-superpowers/hooks/ponytail/ponytail-statusline.sh new file mode 100755 index 00000000..95f75c74 --- /dev/null +++ b/plugins/techletes-superpowers/hooks/ponytail/ponytail-statusline.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# CLAUDE_CONFIG_DIR overrides ~/.claude, matching where the hooks write the flag (issue #34) +flag="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.ponytail-active" +[ -f "$flag" ] || exit 0 + +mode=$(head -n1 "$flag" | tr -d '[:space:]') + +if [ -z "$mode" ] || [ "$mode" = "full" ]; then + printf '\033[38;5;108m[PONYTAIL]\033[0m' +else + printf '\033[38;5;108m[PONYTAIL:%s]\033[0m' "$(printf '%s' "$mode" | tr '[:lower:]' '[:upper:]')" +fi diff --git a/plugins/techletes-superpowers/hooks/ponytail/ponytail-subagent.js b/plugins/techletes-superpowers/hooks/ponytail/ponytail-subagent.js new file mode 100644 index 00000000..96cd10ba --- /dev/null +++ b/plugins/techletes-superpowers/hooks/ponytail/ponytail-subagent.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node +// ponytail โ€” Claude Code SubagentStart hook +// +// SessionStart context is parent-thread only and never reaches subagents, so +// without this every Task-spawned agent runs ponytail-unaware (issue #252). +// When ponytail mode is active, inject the same ruleset into each subagent. + +const { getPonytailInstructions } = require('./ponytail-instructions'); +const { readMode, writeHookOutput } = require('./ponytail-runtime'); + +const mode = readMode(); + +// Absent flag or off โ†’ ponytail isn't active; inject nothing. +if (!mode || mode === 'off') { + process.exit(0); +} + +try { + writeHookOutput('SubagentStart', mode, getPonytailInstructions(mode)); +} catch (e) { + // Silent fail โ€” a stdout error at hook exit must not surface as a hook failure. +} diff --git a/plugins/techletes-superpowers/skills/brainstorming/SKILL.md b/plugins/techletes-superpowers/skills/brainstorming/SKILL.md new file mode 100644 index 00000000..b0d52b25 --- /dev/null +++ b/plugins/techletes-superpowers/skills/brainstorming/SKILL.md @@ -0,0 +1,159 @@ +--- +name: brainstorming +description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation." +--- + +# Brainstorming Ideas Into Designs + +Help turn ideas into fully formed designs and specs through natural collaborative dialogue. + +Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval. + + +Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity. + + +## Anti-Pattern: "This Is Too Simple To Need A Design" + +Every project goes through this process. A todo list, a single-function utility, a config change โ€” all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval. + +## Checklist + +You MUST create a task for each of these items and complete them in order: + +1. **Explore project context** โ€” check files, docs, recent commits +2. **Offer the visual companion just-in-time** โ€” NOT upfront. The first time a question would genuinely be clearer shown than described, offer it then (its own message); on approval its browser tab opens for you. If no visual question ever arises, never offer it. See the Visual Companion section below. +3. **Ask clarifying questions** โ€” one at a time, understand purpose/constraints/success criteria +4. **Propose 2-3 approaches** โ€” with trade-offs and your recommendation +5. **Present design** โ€” in sections scaled to their complexity, get user approval after each section +6. **Write design doc** โ€” save to `docs/superpowers/specs/YYYY-MM-DD--design.md` and commit +7. **Spec self-review** โ€” quick inline check for placeholders, contradictions, ambiguity, scope (see below) +8. **User reviews written spec** โ€” ask user to review the spec file before proceeding +9. **Transition to implementation** โ€” invoke writing-plans skill to create implementation plan + +## Process Flow + +```dot +digraph brainstorming { + "Explore project context" [shape=box]; + "Ask clarifying questions" [shape=box]; + "Propose 2-3 approaches" [shape=box]; + "Present design sections" [shape=box]; + "User approves design?" [shape=diamond]; + "Write design doc" [shape=box]; + "Spec self-review\n(fix inline)" [shape=box]; + "User reviews spec?" [shape=diamond]; + "Invoke writing-plans skill" [shape=doublecircle]; + + "Explore project context" -> "Ask clarifying questions"; + "Ask clarifying questions" -> "Propose 2-3 approaches"; + "Propose 2-3 approaches" -> "Present design sections"; + "Present design sections" -> "User approves design?"; + "User approves design?" -> "Present design sections" [label="no, revise"]; + "User approves design?" -> "Write design doc" [label="yes"]; + "Write design doc" -> "Spec self-review\n(fix inline)"; + "Spec self-review\n(fix inline)" -> "User reviews spec?"; + "User reviews spec?" -> "Write design doc" [label="changes requested"]; + "User reviews spec?" -> "Invoke writing-plans skill" [label="approved"]; +} +``` + +**The terminal state is invoking writing-plans.** Do NOT invoke frontend-design, mcp-builder, or any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans. + +## The Process + +**Understanding the idea:** + +- Check out the current project state first (files, docs, recent commits) +- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first. +- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec โ†’ plan โ†’ implementation cycle. +- For appropriately-scoped projects, ask questions one at a time to refine the idea +- Prefer multiple choice questions when possible, but open-ended is fine too +- Only one question per message - if a topic needs more exploration, break it into multiple questions +- Focus on understanding: purpose, constraints, success criteria + +**Exploring approaches:** + +- Propose 2-3 different approaches with trade-offs +- Present options conversationally with your recommendation and reasoning +- Lead with your recommended option and explain why + +**Presenting the design:** + +- Once you believe you understand what you're building, present the design +- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced +- Ask after each section whether it looks right so far +- Cover: architecture, components, data flow, error handling, testing +- Be ready to go back and clarify if something doesn't make sense + +**Design for isolation and clarity:** + +- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently +- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on? +- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work. +- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much. + +**Working in existing codebases:** + +- Explore the current structure before proposing changes. Follow existing patterns. +- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in. +- Don't propose unrelated refactoring. Stay focused on what serves the current goal. + +## After the Design + +**Documentation:** + +- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD--design.md` + - (User preferences for spec location override this default) +- Use elements-of-style:writing-clearly-and-concisely skill if available +- Commit the design document to git + +**Spec Self-Review:** +After writing the spec document, look at it with fresh eyes: + +1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them. +2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions? +3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition? +4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit. + +Fix any issues inline. No need to re-review โ€” just fix and move on. + +**User Review Gate:** +After the spec review loop passes, ask the user to review the written spec before proceeding: + +> "Spec written and committed to ``. Please review it and let me know if you want to make any changes before we start writing out the implementation plan." + +Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves. + +**Implementation:** + +- Invoke the writing-plans skill to create a detailed implementation plan +- Do NOT invoke any other skill. writing-plans is the next step. + +## Key Principles + +- **One question at a time** - Don't overwhelm with multiple questions +- **Multiple choice preferred** - Easier to answer than open-ended when possible +- **YAGNI ruthlessly** - Remove unnecessary features from all designs +- **Explore alternatives** - Always propose 2-3 approaches before settling +- **Incremental validation** - Present design, get approval before moving on +- **Be flexible** - Go back and clarify when something doesn't make sense + +## Visual Companion + +A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool โ€” not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser. + +**Offering the companion (just-in-time):** Do NOT offer it upfront. Wait until a question would genuinely be clearer shown than told โ€” a real mockup / layout / diagram question, not merely a UI *topic*. The first time that happens, offer it then, as its own message: +> "This next part might be easier if I show you โ€” I can put together mockups, diagrams, and comparisons in a browser tab as we go. It's still new and can be token-intensive. Want me to? I'll open it for you." + +**This offer MUST be its own message.** Only the offer โ€” no clarifying question, summary, or other content. Wait for the user's response. If they accept, start the server with `--open` so their browser opens to the first screen automatically. If they decline, continue text-only and don't offer again unless they raise it. + +**Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?** + +- **Use the browser** for content that IS visual โ€” mockups, wireframes, layout comparisons, architecture diagrams, side-by-side visual designs +- **Use the terminal** for content that is text โ€” requirements questions, conceptual choices, tradeoff lists, A/B/C/D text options, scope decisions + +A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question โ€” use the terminal. "Which wizard layout works better?" is a visual question โ€” use the browser. + +If they agree to the companion, read the detailed guide before proceeding: +`skills/brainstorming/visual-companion.md` diff --git a/plugins/techletes-superpowers/skills/brainstorming/scripts/frame-template.html b/plugins/techletes-superpowers/skills/brainstorming/scripts/frame-template.html new file mode 100644 index 00000000..f540bb8a --- /dev/null +++ b/plugins/techletes-superpowers/skills/brainstorming/scripts/frame-template.html @@ -0,0 +1,213 @@ + + + + + Superpowers Brainstorming + + + +
+ +
Connectingโ€ฆ
+
+ +
+
+ +
+
+ + + diff --git a/plugins/techletes-superpowers/skills/brainstorming/scripts/helper.js b/plugins/techletes-superpowers/skills/brainstorming/scripts/helper.js new file mode 100644 index 00000000..e11d2648 --- /dev/null +++ b/plugins/techletes-superpowers/skills/brainstorming/scripts/helper.js @@ -0,0 +1,167 @@ +(function() { + const MIN_RECONNECT_MS = 500; + const MAX_RECONNECT_MS = 30000; + const TOMBSTONE_AFTER_MS = 15000; // show the "paused" overlay after this long disconnected + + // Pure: next backoff delay (doubles, capped). Exported for unit tests. + function nextReconnectDelay(current, max) { + return Math.min(current * 2, max); + } + if (typeof module !== 'undefined' && module.exports) { + module.exports = { nextReconnectDelay, MIN_RECONNECT_MS, MAX_RECONNECT_MS, TOMBSTONE_AFTER_MS }; + } + + // Everything below is browser-only; bail out when loaded in Node (tests). + if (typeof window === 'undefined') return; + + let ws = null; + let eventQueue = []; + let reconnectDelay = MIN_RECONNECT_MS; + let reconnectTimer = null; + let disconnectedSince = null; + let everConnected = false; + let tombstoneShown = false; + + function sessionKey() { + try { + return window.sessionStorage && window.sessionStorage.getItem('brainstorm-session-key'); + } catch (e) {} + return null; + } + + function websocketUrl() { + const key = sessionKey(); + return 'ws://' + window.location.host + (key ? '/?key=' + encodeURIComponent(key) : ''); + } + + function reloadAfterRecovery() { + const key = sessionKey(); + if (key) { + window.location.replace('/?key=' + encodeURIComponent(key)); + } else { + window.location.reload(); + } + } + + // Reflect connection state in the frame's status pill (absent on full-doc screens). + function setStatus(state) { + const el = document.querySelector('.status'); + if (!el) return; + const map = { + connecting: ['Connectingโ€ฆ', 'var(--text-tertiary)'], + connected: ['Connected', 'var(--success)'], + reconnecting: ['Reconnectingโ€ฆ', 'var(--warning)'], + disconnected: ['Disconnected', 'var(--error)'] + }; + const [text, color] = map[state] || map.disconnected; + el.textContent = text; + el.style.setProperty('--status-color', color); + } + + // Self-styled so it works on framed and full-document screens alike. + function showTombstone() { + if (tombstoneShown) return; + tombstoneShown = true; + const el = document.createElement('div'); + el.id = 'bs-tombstone'; + el.style.cssText = 'position:fixed;inset:0;z-index:99999;display:flex;' + + 'align-items:center;justify-content:center;padding:2rem;text-align:center;' + + 'background:rgba(20,20,22,0.92);color:#f5f5f7;font-family:system-ui,sans-serif'; + el.innerHTML = '
' + + '

Companion paused

' + + '

This brainstorm companion has stopped. ' + + 'Ask your coding agent to bring it back โ€” this page reconnects automatically.

'; + if (document.body) document.body.appendChild(el); + } + + function connect() { + if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } + setStatus(everConnected ? 'reconnecting' : 'connecting'); + ws = new WebSocket(websocketUrl()); + + ws.onopen = () => { + const recovered = tombstoneShown; + everConnected = true; + disconnectedSince = null; + reconnectDelay = MIN_RECONNECT_MS; + tombstoneShown = false; + setStatus('connected'); + eventQueue.forEach(e => ws.send(JSON.stringify(e))); + eventQueue = []; + // Recovered from a tombstoned outage (e.g. the server restarted on the same + // port) โ€” reload through the keyed bootstrap when possible so the cookie is + // refreshed before the visible URL returns to bare /. + if (recovered) reloadAfterRecovery(); + }; + + ws.onmessage = (msg) => { + let data; + try { data = JSON.parse(msg.data); } catch (e) { return; } + if (data.type === 'reload') window.location.reload(); + }; + + ws.onclose = () => { + ws = null; + if (disconnectedSince === null) disconnectedSince = Date.now(); + if (Date.now() - disconnectedSince >= TOMBSTONE_AFTER_MS) { + setStatus('disconnected'); + showTombstone(); + } else { + setStatus('reconnecting'); + } + reconnectTimer = setTimeout(connect, reconnectDelay); + reconnectDelay = nextReconnectDelay(reconnectDelay, MAX_RECONNECT_MS); + }; + + // Let onclose own reconnection so we don't schedule it twice. + ws.onerror = () => { try { ws.close(); } catch (e) {} }; + } + + function sendEvent(event) { + event.timestamp = Date.now(); + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(event)); + } else { + eventQueue.push(event); + } + } + + // Capture clicks on choice elements + document.addEventListener('click', (e) => { + const target = e.target.closest('[data-choice]'); + if (!target) return; + + sendEvent({ + type: 'click', + text: target.textContent.trim(), + choice: target.dataset.choice, + id: target.id || null + }); + + }); + + // Frame UI: selection tracking + window.selectedChoice = null; + + window.toggleSelect = function(el) { + const container = el.closest('.options') || el.closest('.cards'); + const multi = container && container.dataset.multiselect !== undefined; + if (container && !multi) { + container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected')); + } + if (multi) { + el.classList.toggle('selected'); + } else { + el.classList.add('selected'); + } + window.selectedChoice = el.dataset.choice; + }; + + // Expose API for explicit use + window.brainstorm = { + send: sendEvent, + choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata }) + }; + + connect(); +})(); diff --git a/plugins/techletes-superpowers/skills/brainstorming/scripts/server.cjs b/plugins/techletes-superpowers/skills/brainstorming/scripts/server.cjs new file mode 100644 index 00000000..a828b35a --- /dev/null +++ b/plugins/techletes-superpowers/skills/brainstorming/scripts/server.cjs @@ -0,0 +1,723 @@ +const crypto = require('crypto'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +// ========== WebSocket Protocol (RFC 6455) ========== + +const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A }; +const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; +const MAX_FRAME_PAYLOAD_BYTES = 10 * 1024 * 1024; + +function computeAcceptKey(clientKey) { + return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64'); +} + +function encodeFrame(opcode, payload) { + const fin = 0x80; + const len = payload.length; + let header; + + if (len < 126) { + header = Buffer.alloc(2); + header[0] = fin | opcode; + header[1] = len; + } else if (len < 65536) { + header = Buffer.alloc(4); + header[0] = fin | opcode; + header[1] = 126; + header.writeUInt16BE(len, 2); + } else { + header = Buffer.alloc(10); + header[0] = fin | opcode; + header[1] = 127; + header.writeBigUInt64BE(BigInt(len), 2); + } + + return Buffer.concat([header, payload]); +} + +function decodeFrame(buffer) { + if (buffer.length < 2) return null; + + const secondByte = buffer[1]; + const opcode = buffer[0] & 0x0F; + const masked = (secondByte & 0x80) !== 0; + let payloadLen = secondByte & 0x7F; + let offset = 2; + + if (!masked) throw new Error('Client frames must be masked'); + + if (payloadLen === 126) { + if (buffer.length < 4) return null; + payloadLen = buffer.readUInt16BE(2); + offset = 4; + } else if (payloadLen === 127) { + if (buffer.length < 10) return null; + const extendedLen = buffer.readBigUInt64BE(2); + if (extendedLen > BigInt(MAX_FRAME_PAYLOAD_BYTES)) { + throw new Error('WebSocket frame payload exceeds maximum allowed size'); + } + payloadLen = Number(extendedLen); + offset = 10; + } + + if (payloadLen > MAX_FRAME_PAYLOAD_BYTES) { + throw new Error('WebSocket frame payload exceeds maximum allowed size'); + } + + const maskOffset = offset; + const dataOffset = offset + 4; + const totalLen = dataOffset + payloadLen; + if (buffer.length < totalLen) return null; + + const mask = buffer.slice(maskOffset, dataOffset); + const data = Buffer.alloc(payloadLen); + for (let i = 0; i < payloadLen; i++) { + data[i] = buffer[dataOffset + i] ^ mask[i % 4]; + } + + return { opcode, payload: data, bytesConsumed: totalLen }; +} + +// ========== Configuration ========== + +const PORT_FILE = process.env.BRAINSTORM_PORT_FILE || null; +const randomPort = () => 49152 + Math.floor(Math.random() * 16383); +// Prefer an explicit port, else the port this session last bound (so a restart +// reuses it and an already-open browser tab reconnects), else a random high port. +function preferredPort() { + if (process.env.BRAINSTORM_PORT) return Number(process.env.BRAINSTORM_PORT); + if (PORT_FILE) { + try { + const p = Number(fs.readFileSync(PORT_FILE, 'utf-8').trim()); + if (Number.isInteger(p) && p > 1023 && p < 65536) return p; + } catch (e) { /* no prior port recorded */ } + } + return randomPort(); +} +let PORT = preferredPort(); +const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1'; +const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST); +const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm'; +const CONTENT_DIR = path.join(SESSION_DIR, 'content'); +const STATE_DIR = path.join(SESSION_DIR, 'state'); +const SUPERPOWERS_VERSION = readSuperpowersVersion(); +const SUPERPOWERS_BRAND_IMAGE_URL = 'https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png'; +const TELEMETRY_DISABLE_ENV_VARS = [ + 'SUPERPOWERS_DISABLE_TELEMETRY', + 'DISABLE_TELEMETRY', + 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC' +]; +const SUPERPOWERS_TELEMETRY_DISABLED = TELEMETRY_DISABLE_ENV_VARS.some(name => isTruthyEnv(process.env[name])); +let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null; + +// Per-session secret key. The companion is reachable by any local browser tab +// and, when bound to a non-loopback host, by any host that can route to it. +// The key authenticates the real client uniformly across loopback, tunnel, and +// remote binds โ€” and defeats DNS rebinding โ€” where a Host/Origin allowlist +// cannot. It rides the served URL as ?key= and is mirrored into a cookie on +// first load so same-origin subresources and the WebSocket carry it for free. +// Persisted alongside the port (BRAINSTORM_TOKEN_FILE) so a restart keeps the +// same key and an already-open tab's cookie still validates. +const TOKEN_FILE = process.env.BRAINSTORM_TOKEN_FILE || null; +function generateToken() { + return crypto.randomBytes(32).toString('hex'); +} + +function chmodOwnerOnly(file) { + try { fs.chmodSync(file, 0o600); } catch (e) { /* best effort */ } +} + +function initialToken() { + if (process.env.BRAINSTORM_TOKEN) { + return { value: process.env.BRAINSTORM_TOKEN, source: 'env' }; + } + if (TOKEN_FILE) { + try { + const t = fs.readFileSync(TOKEN_FILE, 'utf-8').trim(); + if (/^[0-9a-f]{32,}$/i.test(t)) { + chmodOwnerOnly(TOKEN_FILE); + return { value: t, source: 'file' }; + } + } catch (e) { /* no prior token recorded */ } + } + return { value: generateToken(), source: 'generated' }; +} + +const tokenInfo = initialToken(); +let TOKEN = tokenInfo.value; +let tokenSource = tokenInfo.source; +let COOKIE_NAME = 'brainstorm-key-' + PORT; // refined to the actual bound port in onListen + +const MIME_TYPES = { + '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', + '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml' +}; + +// ========== Templates and Constants ========== + +function waitingPage() { + return renderBranding(` + +Brainstorm Companion + + +

Brainstorm Companion

+

Waiting for the agent to push a screen...

`); +} + +const FORBIDDEN_PAGE = ` + +Session key required + + +

Session key required

+

This page needs the full URL your coding agent gave you, including the +?key=… part. Copy the complete URL and open it again.

`; + +function bootstrapPage(key) { + const jsonKey = JSON.stringify(String(key)); + return ` + +Opening Brainstorm Companion + + + +`; +} + +const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8'); +const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8'); +const helperInjection = ''; + +// ========== Helper Functions ========== + +function readSuperpowersVersion() { + const root = path.join(__dirname, '../../..'); + const manifests = [ + path.join(root, 'package.json'), + path.join(root, '.codex-plugin/plugin.json') + ]; + + for (const manifest of manifests) { + try { + const data = JSON.parse(fs.readFileSync(manifest, 'utf-8')); + if (data.version) return String(data.version); + } catch (e) { + // Packaged Codex plugins omit package.json; try the next manifest. + } + } + + return 'unknown'; +} + +function isTruthyEnv(value) { + if (!value) return false; + const normalized = String(value).trim().toLowerCase(); + if (!normalized) return false; + return !['0', 'false', 'no', 'off'].includes(normalized); +} + +function escapeHtmlText(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function brandMarkup() { + const version = escapeHtmlText(SUPERPOWERS_VERSION); + const text = SUPERPOWERS_TELEMETRY_DISABLED + ? 'Prime Radiant Superpowers v' + version + : 'Superpowers v' + version; + const logo = SUPERPOWERS_TELEMETRY_DISABLED + ? '' + : ''; + + return ''; +} + +function renderBranding(html) { + return html.split('').join(brandMarkup()); +} + +function isFullDocument(html) { + const trimmed = html.trimStart().toLowerCase(); + return trimmed.startsWith('', content); +} + +function getNewestScreen() { + const files = fs.readdirSync(CONTENT_DIR) + .filter(f => !f.startsWith('.') && f.endsWith('.html')) + .map(f => { + const fp = path.join(CONTENT_DIR, f); + if (!isRegularFileInsideContentDir(fp)) return null; + return { path: fp, mtime: fs.statSync(fp).mtime.getTime() }; + }) + .filter(Boolean) + .sort((a, b) => b.mtime - a.mtime); + return files.length > 0 ? files[0].path : null; +} + +function urlHostForHttp(host) { + const h = String(host); + if (h.startsWith('[') && h.endsWith(']')) return h; + return h.includes(':') ? '[' + h + ']' : h; +} + +function companionUrl() { + return 'http://' + urlHostForHttp(URL_HOST) + ':' + PORT + '/?key=' + TOKEN; +} + +function browserLauncherForPlatform(url, { + platform = process.platform, + osRelease = require('os').release(), + env = process.env +} = {}) { + const isWSL = platform === 'linux' && /microsoft/i.test(osRelease); + if (platform === 'darwin') return { bin: 'open', args: [url] }; + if (platform === 'win32' || isWSL) { + return { bin: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', url] }; + } + if (env.DISPLAY || env.WAYLAND_DISPLAY) return { bin: 'xdg-open', args: [url] }; + return null; +} + +function isRegularFileInsideContentDir(filePath) { + let stat, realContentDir, realFilePath; + try { + stat = fs.lstatSync(filePath); + if (stat.isSymbolicLink()) return false; + if (!stat.isFile()) return false; + if (stat.nlink !== 1) return false; + realContentDir = fs.realpathSync(CONTENT_DIR); + realFilePath = fs.realpathSync(filePath); + } catch (e) { + return false; + } + return realFilePath.startsWith(realContentDir + path.sep); +} + +// ========== Authentication ========== + +function timingSafeEqualStr(a, b) { + const ab = Buffer.from(String(a)); + const bb = Buffer.from(String(b)); + if (ab.length !== bb.length) return false; + return crypto.timingSafeEqual(ab, bb); +} + +function parseCookies(header) { + const out = {}; + if (!header) return out; + for (const part of header.split(';')) { + const eq = part.indexOf('='); + if (eq < 0) continue; + out[part.slice(0, eq).trim()] = part.slice(eq + 1).trim(); + } + return out; +} + +// A request is authorized if it carries the session key as ?key= or as the +// session cookie. Both are compared in constant time. +function isAuthorized(req) { + const q = req.url.indexOf('?'); + if (q >= 0) { + const params = new URLSearchParams(req.url.slice(q + 1)); + if (params.has('key')) { + const key = params.get('key'); + return Boolean(key && timingSafeEqualStr(key, TOKEN)); + } + } + const cookie = parseCookies(req.headers['cookie'])[COOKIE_NAME]; + if (cookie && timingSafeEqualStr(cookie, TOKEN)) return true; + return false; +} + +function pathnameOf(url) { + const q = url.indexOf('?'); + return q >= 0 ? url.slice(0, q) : url; +} + +function queryKey(url) { + const q = url.indexOf('?'); + if (q < 0) return null; + return new URLSearchParams(url.slice(q + 1)).get('key'); +} + +function securityHeaders(headers = {}) { + return { + 'Referrer-Policy': 'no-referrer', + 'Cache-Control': 'no-store', + 'X-Frame-Options': 'DENY', + 'Content-Security-Policy': "frame-ancestors 'none'", + 'Cross-Origin-Resource-Policy': 'same-origin', + ...headers + }; +} + +function isAllowedWebSocketOrigin(req) { + const origin = req.headers.origin; + if (!origin) return true; + const host = req.headers.host; + if (!host) return false; + return origin === 'http://' + host; +} + +// ========== HTTP Request Handler ========== + +function handleRequest(req, res) { + if (!isAuthorized(req)) { + res.writeHead(403, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' })); + res.end(FORBIDDEN_PAGE); + return; + } + touchActivity(); // only authorized requests count as activity + + // Mirror the key into a cookie so same-origin subresources (/files/*) can + // authenticate after bootstrap. HttpOnly keeps it away from page scripts; the + // WebSocket Origin check below is what blocks cross-origin localhost injection. + res.setHeader('Set-Cookie', + COOKIE_NAME + '=' + TOKEN + '; HttpOnly; SameSite=Strict; Path=/'); + + const pathname = pathnameOf(req.url); + const keyFromQuery = queryKey(req.url); + if (req.method === 'GET' && pathname === '/' && keyFromQuery && timingSafeEqualStr(keyFromQuery, TOKEN)) { + res.writeHead(200, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' })); + res.end(bootstrapPage(keyFromQuery)); + } else if (req.method === 'GET' && pathname === '/') { + const screenFile = getNewestScreen(); + let html = screenFile + ? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8')) + : waitingPage(); + + if (html.includes('')) { + html = html.replace('', helperInjection + '\n'); + } else { + html += helperInjection; + } + + res.writeHead(200, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' })); + res.end(html); + } else if (req.method === 'GET' && pathname.startsWith('/files/')) { + const fileName = path.basename(pathname.slice(7)); + const filePath = path.join(CONTENT_DIR, fileName); + // Reject empty/dotfile names and anything that isn't a regular file โ€” + // `/files/` would otherwise resolve to CONTENT_DIR and crash readFileSync (EISDIR). + if (!fileName || fileName.startsWith('.') || !isRegularFileInsideContentDir(filePath)) { + res.writeHead(404, securityHeaders()); + res.end('Not found'); + return; + } + const ext = path.extname(filePath).toLowerCase(); + const contentType = MIME_TYPES[ext] || 'application/octet-stream'; + res.writeHead(200, securityHeaders({ 'Content-Type': contentType })); + res.end(fs.readFileSync(filePath)); + } else { + res.writeHead(404, securityHeaders()); + res.end('Not found'); + } +} + +// ========== WebSocket Connection Handling ========== + +const clients = new Set(); + +function handleUpgrade(req, socket) { + if (!isAuthorized(req) || !isAllowedWebSocketOrigin(req)) { socket.destroy(); return; } + + const key = req.headers['sec-websocket-key']; + if (!key) { socket.destroy(); return; } + + const accept = computeAcceptKey(key); + socket.write( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + 'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n' + ); + + let buffer = Buffer.alloc(0); + clients.add(socket); + + socket.on('data', (chunk) => { + buffer = Buffer.concat([buffer, chunk]); + while (buffer.length > 0) { + let result; + try { + result = decodeFrame(buffer); + } catch (e) { + socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0))); + clients.delete(socket); + return; + } + if (!result) break; + buffer = buffer.slice(result.bytesConsumed); + + switch (result.opcode) { + case OPCODES.TEXT: + handleMessage(result.payload.toString()); + break; + case OPCODES.CLOSE: + socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0))); + clients.delete(socket); + return; + case OPCODES.PING: + socket.write(encodeFrame(OPCODES.PONG, result.payload)); + break; + case OPCODES.PONG: + break; + default: { + const closeBuf = Buffer.alloc(2); + closeBuf.writeUInt16BE(1003); + socket.end(encodeFrame(OPCODES.CLOSE, closeBuf)); + clients.delete(socket); + return; + } + } + } + }); + + socket.on('close', () => clients.delete(socket)); + socket.on('error', () => clients.delete(socket)); +} + +function handleMessage(text) { + let event; + try { + event = JSON.parse(text); + } catch (e) { + console.error('Failed to parse WebSocket message:', e.message); + return; + } + touchActivity(); + console.log(JSON.stringify({ source: 'user-event', ...event })); + if (event && event.choice) { + const eventsFile = path.join(STATE_DIR, 'events'); + fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n'); + } +} + +function broadcast(msg) { + const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg))); + for (const socket of clients) { + try { socket.write(frame); } catch (e) { clients.delete(socket); } + } +} + +// Best-effort: open the user's browser the first time a screen is actually ready +// to show. Skips when disabled, on a non-loopback (remote) bind, or when a +// browser is already connected. Override the launcher with BRAINSTORM_OPEN_CMD. +let browserOpened = false; +function maybeOpenBrowser() { + if (browserOpened) return; + browserOpened = true; + if (!process.env.BRAINSTORM_OPEN) return; // opt-in: only after the user approves the companion + if (HOST !== '127.0.0.1' && HOST !== 'localhost') return; + if (clients.size > 0) return; // the user already opened it + const url = companionUrl(); // must carry the key or the gate 403s it + const cp = require('child_process'); + // Operator-provided launcher: run as given (this env var is trusted operator input). + if (process.env.BRAINSTORM_OPEN_CMD) { + try { cp.exec(process.env.BRAINSTORM_OPEN_CMD + ' ' + JSON.stringify(url), () => {}); } catch (e) { /* best effort */ } + return; + } + // Platform launchers: pass the URL as an argv element via execFile (no shell), + // so a url-host containing shell metacharacters can't inject a command. + const launcher = browserLauncherForPlatform(url); + if (!launcher) return; // headless: nothing to open + try { cp.execFile(launcher.bin, launcher.args, () => {}); } catch (e) { /* best effort */ } +} + +// ========== Activity Tracking ========== + +// Idle timeout: shut down after this long with no activity. Default 4 hours; +// override with BRAINSTORM_IDLE_TIMEOUT_MS (start-server.sh: --idle-timeout-minutes). +const IDLE_TIMEOUT_MS = (() => { + const ms = Number(process.env.BRAINSTORM_IDLE_TIMEOUT_MS); + return Number.isFinite(ms) && ms > 0 ? ms : 4 * 60 * 60 * 1000; +})(); +// How often the watchdog checks for owner-death / idleness. Configurable mainly +// so tests can run fast; production default is 60s. +const LIFECYCLE_CHECK_MS = (() => { + const ms = Number(process.env.BRAINSTORM_LIFECYCLE_CHECK_MS); + return Number.isFinite(ms) && ms > 0 ? ms : 60 * 1000; +})(); +let lastActivity = Date.now(); + +function touchActivity() { + lastActivity = Date.now(); +} + +// ========== File Watching ========== + +const debounceTimers = new Map(); + +// ========== Server Startup ========== + +function startServer() { + if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true }); + if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); + + // Track known files to distinguish new screens from updates. + // macOS fs.watch reports 'rename' for both new files and overwrites, + // so we can't rely on eventType alone. + const knownFiles = new Set( + fs.readdirSync(CONTENT_DIR).filter(f => !f.startsWith('.') && f.endsWith('.html')) + ); + + const server = http.createServer(handleRequest); + server.on('upgrade', handleUpgrade); + + const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => { + if (!filename || filename.startsWith('.') || !filename.endsWith('.html')) return; + + if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename)); + debounceTimers.set(filename, setTimeout(() => { + debounceTimers.delete(filename); + const filePath = path.join(CONTENT_DIR, filename); + + if (!fs.existsSync(filePath)) return; // file was deleted + touchActivity(); + + if (!knownFiles.has(filename)) { + knownFiles.add(filename); + const eventsFile = path.join(STATE_DIR, 'events'); + if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); + console.log(JSON.stringify({ type: 'screen-added', file: filePath })); + maybeOpenBrowser(); + } else { + console.log(JSON.stringify({ type: 'screen-updated', file: filePath })); + } + + broadcast({ type: 'reload' }); + }, 100)); + }); + watcher.on('error', (err) => console.error('fs.watch error:', err.message)); + + function shutdown(reason) { + console.log(JSON.stringify({ type: 'server-stopped', reason })); + const infoFile = path.join(STATE_DIR, 'server-info'); + if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile); + fs.writeFileSync( + path.join(STATE_DIR, 'server-stopped'), + JSON.stringify({ reason, timestamp: Date.now() }) + '\n' + ); + watcher.close(); + clearInterval(lifecycleCheck); + // Close any upgraded WebSocket sockets so server.close() can complete and + // the process actually exits instead of lingering on an open connection. + for (const socket of clients) { + try { socket.destroy(); } catch (e) { /* already gone */ } + } + server.close(() => process.exit(0)); + } + + function ownerAlive() { + if (!ownerPid) return true; + try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; } + } + + // Periodically exit if the owner process died or we've been idle too long. + const lifecycleCheck = setInterval(() => { + if (!ownerAlive()) shutdown('owner process exited'); + else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown('idle timeout'); + }, LIFECYCLE_CHECK_MS); + lifecycleCheck.unref(); + + // Validate owner PID at startup. If it's already dead, the PID resolution + // was wrong (common on WSL, Tailscale SSH, and cross-user scenarios). + // Disable monitoring and rely on the idle timeout instead. + if (ownerPid) { + try { process.kill(ownerPid, 0); } + catch (e) { + if (e.code !== 'EPERM') { + console.log(JSON.stringify({ type: 'owner-pid-invalid', pid: ownerPid, reason: 'dead at startup' })); + ownerPid = null; + } + } + } + + // If the preferred port is already taken (e.g. a previous server is still + // alive), fall back to a random port once instead of failing. + let triedFallback = false; + + function onListen() { + // Cookie name keys on the ACTUAL bound port (may differ from the preferred + // one after an EADDRINUSE fallback) so it can't collide with another server's + // cookie in the shared localhost jar. + COOKIE_NAME = 'brainstorm-key-' + PORT; + // Record the bound port AND token so the next restart of this session reuses + // them โ€” but ONLY when we got our preferred port. On a fallback we bound a + // *different* port because someone else holds the preferred one; persisting + // would overwrite the shared files and strand that other session's open tab. + if (PORT_FILE && !triedFallback) { + try { fs.writeFileSync(PORT_FILE, String(PORT)); } catch (e) { /* best effort */ } + if (TOKEN_FILE) { + try { + fs.writeFileSync(TOKEN_FILE, TOKEN, { mode: 0o600 }); + chmodOwnerOnly(TOKEN_FILE); + } catch (e) { /* best effort */ } + } + } + const info = JSON.stringify({ + type: 'server-started', port: Number(PORT), host: HOST, + url_host: URL_HOST, url: companionUrl(), + screen_dir: CONTENT_DIR, state_dir: STATE_DIR, idle_timeout_ms: IDLE_TIMEOUT_MS + }); + console.log(info); + // server-info embeds the key โ€” keep it owner-only. + fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n', { mode: 0o600 }); + } + + server.on('error', (err) => { + if (err.code === 'EADDRINUSE' && !triedFallback) { + if (tokenSource === 'env') { + console.error('Server failed to bind: preferred port is in use and BRAINSTORM_TOKEN is set; refusing fallback with explicit token'); + process.exit(1); + } + triedFallback = true; + PORT = randomPort(); + if (tokenSource === 'file') { + TOKEN = generateToken(); + tokenSource = 'generated-fallback'; + } + server.listen(PORT, HOST, onListen); + } else { + console.error('Server failed to bind:', err.message); + process.exit(1); + } + }); + server.listen(PORT, HOST, onListen); +} + +if (require.main === module) { + startServer(); +} + +module.exports = { + computeAcceptKey, + encodeFrame, + decodeFrame, + browserLauncherForPlatform, + OPCODES, + MAX_FRAME_PAYLOAD_BYTES +}; diff --git a/plugins/techletes-superpowers/skills/brainstorming/scripts/start-server.sh b/plugins/techletes-superpowers/skills/brainstorming/scripts/start-server.sh new file mode 100755 index 00000000..016a8e48 --- /dev/null +++ b/plugins/techletes-superpowers/skills/brainstorming/scripts/start-server.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# Start the brainstorm server and output connection info +# Usage: start-server.sh [--project-dir ] [--host ] [--url-host ] [--foreground] [--background] +# +# Starts server on a random high port, outputs JSON with URL. +# Each session gets its own directory to avoid conflicts. +# +# Options: +# --project-dir Store session files under /.superpowers/brainstorm/ +# instead of /tmp. Files persist after server stops. +# --host Host/interface to bind (default: 127.0.0.1). +# Use 0.0.0.0 in remote/containerized environments. +# --url-host Hostname shown in returned URL JSON. +# --idle-timeout-minutes Shut down after n minutes idle (default 240 = 4h). +# --open Auto-open the browser on the first screen (use only +# after the user approves the visual companion). +# --foreground Run server in the current terminal (no backgrounding). +# --background Force background mode (overrides Codex auto-foreground). + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Parse arguments +PROJECT_DIR="" +FOREGROUND="false" +FORCE_BACKGROUND="false" +BIND_HOST="127.0.0.1" +URL_HOST="" +IDLE_TIMEOUT_MINUTES="" +while [[ $# -gt 0 ]]; do + case "$1" in + --project-dir) + PROJECT_DIR="$2" + shift 2 + ;; + --host) + BIND_HOST="$2" + shift 2 + ;; + --url-host) + URL_HOST="$2" + shift 2 + ;; + --idle-timeout-minutes) + IDLE_TIMEOUT_MINUTES="$2" + shift 2 + ;; + --open) + export BRAINSTORM_OPEN=1 + shift + ;; + --foreground|--no-daemon) + FOREGROUND="true" + shift + ;; + --background|--daemon) + FORCE_BACKGROUND="true" + shift + ;; + *) + echo "{\"error\": \"Unknown argument: $1\"}" + exit 1 + ;; + esac +done + +if [[ -z "$URL_HOST" ]]; then + if [[ "$BIND_HOST" == "127.0.0.1" || "$BIND_HOST" == "localhost" ]]; then + URL_HOST="localhost" + else + URL_HOST="$BIND_HOST" + fi +fi + +if [[ -n "$IDLE_TIMEOUT_MINUTES" ]]; then + if ! [[ "$IDLE_TIMEOUT_MINUTES" =~ ^[0-9]+$ ]] || [[ "$IDLE_TIMEOUT_MINUTES" -lt 1 ]]; then + echo "{\"error\": \"--idle-timeout-minutes must be a positive integer\"}" + exit 1 + fi + export BRAINSTORM_IDLE_TIMEOUT_MS=$(( IDLE_TIMEOUT_MINUTES * 60 * 1000 )) +fi + +is_windows_like_shell() { + case "${OSTYPE:-}" in + msys*|cygwin*|mingw*) return 0 ;; + esac + if [[ -n "${MSYSTEM:-}" ]]; then + return 0 + fi + local uname_s + uname_s="$(uname -s 2>/dev/null || true)" + case "$uname_s" in + MSYS*|MINGW*|CYGWIN*) return 0 ;; + esac + return 1 +} + +# Some environments reap detached/background processes. Auto-foreground when detected. +if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then + FOREGROUND="true" +fi + +# Windows/Git Bash reaps nohup background processes. Auto-foreground when detected. +if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then + if is_windows_like_shell; then + FOREGROUND="true" + fi +fi + +# Session files (server.log, server-info, .last-token) embed the session key โ€” +# keep everything this script and the server create owner-only. +umask 077 + +# Generate unique session directory +SESSION_ID="$$-$(date +%s)" + +if [[ -n "$PROJECT_DIR" ]]; then + SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}" + # Persist the bound port and key per project so a restart reuses them and an + # already-open browser tab reconnects to the same URL with a valid cookie. + export BRAINSTORM_PORT_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-port" + export BRAINSTORM_TOKEN_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-token" +else + SESSION_DIR="/tmp/brainstorm-${SESSION_ID}" +fi + +STATE_DIR="${SESSION_DIR}/state" +PID_FILE="${STATE_DIR}/server.pid" +LOG_FILE="${STATE_DIR}/server.log" +SERVER_ID_FILE="${STATE_DIR}/server-instance-id" + +# Create fresh session directory with content and state peers +mkdir -p "${SESSION_DIR}/content" "$STATE_DIR" + +SERVER_ID="" +if [[ -r /dev/urandom ]]; then + SERVER_ID="$(od -An -N24 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' || true)" +fi +if ! [[ "$SERVER_ID" =~ ^[A-Za-z0-9_-]{32,64}$ ]]; then + SERVER_ID="$(printf '%08x%08x%08x%08x' "$$" "$(date +%s)" "${RANDOM:-0}" "${RANDOM:-0}")" +fi +printf '%s\n' "$SERVER_ID" > "$SERVER_ID_FILE" +chmod 600 "$SERVER_ID_FILE" 2>/dev/null || true + +# Kill any existing server +if [[ -f "$PID_FILE" ]]; then + old_pid=$(cat "$PID_FILE") + kill "$old_pid" 2>/dev/null + rm -f "$PID_FILE" +fi + +cd "$SCRIPT_DIR" || exit 1 + +# Resolve the harness PID (grandparent of this script). +# $PPID is the ephemeral shell the harness spawned to run us โ€” it dies +# when this script exits. The harness itself is $PPID's parent. +OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')" +if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then + OWNER_PID="$PPID" +fi + +# Windows/MSYS2: Node.js cannot see POSIX PIDs from the MSYS2 namespace. +# Passing a PID node cannot verify causes server to log owner-pid-invalid +# and self-terminate at the 60-second lifecycle check. Clear it so the +# watchdog is disabled and the idle timeout becomes the only shutdown trigger. +if is_windows_like_shell; then + OWNER_PID="" +fi + +# Foreground mode for environments that reap detached/background processes. +if [[ "$FOREGROUND" == "true" ]]; then + env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs "--brainstorm-server-id=$SERVER_ID" & + SERVER_PID=$! + echo "$SERVER_PID" > "$PID_FILE" + wait "$SERVER_PID" + exit $? +fi + +# Start server, capturing output to log file +# Use nohup to survive shell exit; disown to remove from job table +nohup env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs "--brainstorm-server-id=$SERVER_ID" > "$LOG_FILE" 2>&1 & +SERVER_PID=$! +disown "$SERVER_PID" 2>/dev/null +echo "$SERVER_PID" > "$PID_FILE" + +# Wait for server-started message (check log file) +for _ in {1..50}; do + if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then + # Verify server is still alive after a short window (catches process reapers) + alive="true" + for _ in {1..20}; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + alive="false" + break + fi + sleep 0.1 + done + if [[ "$alive" != "true" ]]; then + echo "{\"error\": \"Server started but was killed. Retry in a persistent terminal with: $SCRIPT_DIR/start-server.sh${PROJECT_DIR:+ --project-dir $PROJECT_DIR} --host $BIND_HOST --url-host $URL_HOST --foreground\"}" + exit 1 + fi + grep "server-started" "$LOG_FILE" | head -1 + exit 0 + fi + sleep 0.1 +done + +# Timeout - server didn't start +echo '{"error": "Server failed to start within 5 seconds"}' +exit 1 diff --git a/plugins/techletes-superpowers/skills/brainstorming/scripts/stop-server.sh b/plugins/techletes-superpowers/skills/brainstorming/scripts/stop-server.sh new file mode 100755 index 00000000..7cacfe94 --- /dev/null +++ b/plugins/techletes-superpowers/skills/brainstorming/scripts/stop-server.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Stop the brainstorm server and clean up +# Usage: stop-server.sh +# +# Kills the server process. Only deletes session directory if it's +# under /tmp (ephemeral). Persistent directories (.superpowers/) are +# kept so mockups can be reviewed later. + +SESSION_DIR="$1" + +if [[ -z "$SESSION_DIR" ]]; then + echo '{"error": "Usage: stop-server.sh "}' + exit 1 +fi + +STATE_DIR="${SESSION_DIR}/state" +PID_FILE="${STATE_DIR}/server.pid" +SERVER_ID_FILE="${STATE_DIR}/server-instance-id" + +mark_stopped() { + local reason="$1" + rm -f "${STATE_DIR}/server-info" + printf '{"reason":"%s","timestamp":%s}\n' "$reason" "$(date +%s)" > "${STATE_DIR}/server-stopped" +} + +read_expected_server_id() { + [[ -f "$SERVER_ID_FILE" ]] || return 1 + local id + id="$(tr -d '\r\n' < "$SERVER_ID_FILE" 2>/dev/null || true)" + [[ "$id" =~ ^[A-Za-z0-9_-]{32,64}$ ]] || return 1 + printf '%s\n' "$id" +} + +command_line_for_pid() { + local pid="$1" + if [[ -r "/proc/$pid/cmdline" ]]; then + tr '\0' '\n' < "/proc/$pid/cmdline" 2>/dev/null || true + return 0 + fi + ps -ww -p "$pid" -o command= 2>/dev/null || ps -f -p "$pid" 2>/dev/null | sed '1d' || true +} + +command_has_server_id() { + local pid="$1" + local expected="$2" + local expected_arg="--brainstorm-server-id=$expected" + if [[ -r "/proc/$pid/cmdline" ]]; then + local arg + while IFS= read -r -d '' arg || [[ -n "$arg" ]]; do + [[ "$arg" == "$expected_arg" ]] && return 0 + done < "/proc/$pid/cmdline" + return 1 + fi + local command_line + command_line="$(command_line_for_pid "$pid")" + [[ -n "$command_line" ]] || return 1 + case " $command_line " in + *" $expected_arg "*) return 0 ;; + *) return 1 ;; + esac +} + +# Confirm a PID has this session's per-start instance id, not just a familiar +# process name. Ambiguous or legacy metadata fails closed as stale_pid. +is_brainstorm_server() { + kill -0 "$1" 2>/dev/null || return 1 + local expected_id + expected_id="$(read_expected_server_id)" || return 1 + command_has_server_id "$1" "$expected_id" || return 1 + return 0 +} + +if [[ -f "$PID_FILE" ]]; then + pid=$(cat "$PID_FILE") + + # Refuse to signal a PID we can't prove is our server. A stale pid file may + # point at an unrelated process after a reboot/PID wraparound. + if ! is_brainstorm_server "$pid"; then + rm -f "$PID_FILE" "$SERVER_ID_FILE" + mark_stopped "stale_pid" + echo '{"status": "stale_pid"}' + exit 0 + fi + + # Try to stop gracefully, fallback to force if still alive + kill "$pid" 2>/dev/null || true + + # Wait for graceful shutdown (up to ~2s) + for _ in {1..20}; do + if ! kill -0 "$pid" 2>/dev/null; then + break + fi + sleep 0.1 + done + + # If still running, escalate to SIGKILL + if kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null || true + + # Give SIGKILL a moment to take effect + sleep 0.1 + fi + + if kill -0 "$pid" 2>/dev/null; then + echo '{"status": "failed", "error": "process still running"}' + exit 1 + fi + + rm -f "$PID_FILE" "$SERVER_ID_FILE" "${STATE_DIR}/server.log" + mark_stopped "stop-server.sh" + + # Only delete ephemeral /tmp directories + if [[ "$SESSION_DIR" == /tmp/* ]]; then + rm -rf "$SESSION_DIR" + fi + + echo '{"status": "stopped"}' +else + echo '{"status": "not_running"}' +fi diff --git a/plugins/techletes-superpowers/skills/brainstorming/spec-document-reviewer-prompt.md b/plugins/techletes-superpowers/skills/brainstorming/spec-document-reviewer-prompt.md new file mode 100644 index 00000000..60993129 --- /dev/null +++ b/plugins/techletes-superpowers/skills/brainstorming/spec-document-reviewer-prompt.md @@ -0,0 +1,49 @@ +# Spec Document Reviewer Prompt Template + +Use this template when dispatching a spec document reviewer subagent. + +**Purpose:** Verify the spec is complete, consistent, and ready for implementation planning. + +**Dispatch after:** Spec document is written to docs/superpowers/specs/ + +``` +Subagent (general-purpose): + description: "Review spec document" + prompt: | + You are a spec document reviewer. Verify this spec is complete and ready for planning. + + **Spec to review:** [SPEC_FILE_PATH] + + ## What to Check + + | Category | What to Look For | + |----------|------------------| + | Completeness | TODOs, placeholders, "TBD", incomplete sections | + | Consistency | Internal contradictions, conflicting requirements | + | Clarity | Requirements ambiguous enough to cause someone to build the wrong thing | + | Scope | Focused enough for a single plan โ€” not covering multiple independent subsystems | + | YAGNI | Unrequested features, over-engineering | + + ## Calibration + + **Only flag issues that would cause real problems during implementation planning.** + A missing section, a contradiction, or a requirement so ambiguous it could be + interpreted two different ways โ€” those are issues. Minor wording improvements, + stylistic preferences, and "sections less detailed than others" are not. + + Approve unless there are serious gaps that would lead to a flawed plan. + + ## Output Format + + ## Spec Review + + **Status:** Approved | Issues Found + + **Issues (if any):** + - [Section X]: [specific issue] - [why it matters for planning] + + **Recommendations (advisory, do not block approval):** + - [suggestions for improvement] +``` + +**Reviewer returns:** Status, Issues (if any), Recommendations diff --git a/plugins/techletes-superpowers/skills/brainstorming/visual-companion.md b/plugins/techletes-superpowers/skills/brainstorming/visual-companion.md new file mode 100644 index 00000000..7b89f6b2 --- /dev/null +++ b/plugins/techletes-superpowers/skills/brainstorming/visual-companion.md @@ -0,0 +1,291 @@ +# Visual Companion Guide + +Browser-based visual brainstorming companion for showing mockups, diagrams, and options. + +## When to Use + +Decide per-question, not per-session. The test: **would the user understand this better by seeing it than reading it?** + +**Use the browser** when the content itself is visual: + +- **UI mockups** โ€” wireframes, layouts, navigation structures, component designs +- **Architecture diagrams** โ€” system components, data flow, relationship maps +- **Side-by-side visual comparisons** โ€” comparing two layouts, two color schemes, two design directions +- **Design polish** โ€” when the question is about look and feel, spacing, visual hierarchy +- **Spatial relationships** โ€” state machines, flowcharts, entity relationships rendered as diagrams + +**Use the terminal** when the content is text or tabular: + +- **Requirements and scope questions** โ€” "what does X mean?", "which features are in scope?" +- **Conceptual A/B/C choices** โ€” picking between approaches described in words +- **Tradeoff lists** โ€” pros/cons, comparison tables +- **Technical decisions** โ€” API design, data modeling, architectural approach selection +- **Clarifying questions** โ€” anything where the answer is words, not a visual preference + +A question *about* a UI topic is not automatically a visual question. "What kind of wizard do you want?" is conceptual โ€” use the terminal. "Which of these wizard layouts feels right?" is visual โ€” use the browser. + +## How It Works + +The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn. + +**Content fragments vs full documents:** If your HTML file starts with `/.superpowers/brainstorm/` for the session directory. + +**Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there. + +**Launching the server by platform:** + +**Claude Code:** +```bash +# Default mode works โ€” the script backgrounds the server itself. +scripts/start-server.sh --project-dir /path/to/project --open +``` + +On Windows, the script auto-detects and switches to foreground mode (which blocks the tool call). Use `run_in_background: true` on the Bash tool call so the server survives across conversation turns, then read `$STATE_DIR/server-info` on the next turn to get the URL and port. + +**Codex:** +```bash +# Codex reaps background processes. The script auto-detects CODEX_CI and +# switches to foreground mode. Run it normally โ€” no extra flags needed. +scripts/start-server.sh --project-dir /path/to/project --open +``` + +**Copilot CLI:** +```bash +# Use --foreground and start the server via the bash tool with mode: "async" +# so the process survives across turns. Capture the returned shellId for +# read_bash / stop_bash if you need to interact with it later. +scripts/start-server.sh --project-dir /path/to/project --open --foreground +``` + +**Other environments:** The server must keep running in the background across conversation turns. If your environment reaps detached processes, use `--foreground` and launch the command with your platform's background execution mechanism. + +If the URL is unreachable from your browser (common in remote/containerized setups), bind a non-loopback host: + +```bash +scripts/start-server.sh \ + --project-dir /path/to/project \ + --host 0.0.0.0 \ + --url-host localhost +``` + +Use `--url-host` to control what hostname is printed in the returned URL JSON. + +## The Loop + +1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`: + - **Required: confirm the server is alive before referring to the URL or pushing a screen.** Check that `$STATE_DIR/server-info` exists and `$STATE_DIR/server-stopped` does not. If it has shut down, restart it with `start-server.sh` using the **same `--project-dir`** โ€” it reuses the same port, so the user's open tab reconnects on its own (it shows a "paused" overlay while the server is down) and you don't need to send a new URL. The server auto-exits after 4 hours idle (configurable with `--idle-timeout-minutes`). + - Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html` + - **Never reuse filenames** โ€” each screen gets a fresh file + - Use your file-creation tool โ€” **never use cat/heredoc** (dumps noise into terminal) + - Server automatically serves the newest file + +2. **Tell user what to expect and end your turn:** + - Remind them of the URL (every step, not just first) + - Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage") + - Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like." + +3. **On your next turn** โ€” after the user responds in the terminal: + - Read `$STATE_DIR/events` if it exists โ€” this contains the user's browser interactions (clicks, selections) as JSON lines + - Merge with the user's terminal text to get the full picture + - The terminal message is the primary feedback; `state_dir/events` provides structured interaction data + +4. **Iterate or advance** โ€” if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated. + +5. **Unload when returning to terminal** โ€” when the next step doesn't need the browser (e.g., a clarifying question, a tradeoff discussion), push a waiting screen to clear the stale content: + + ```html + +
+

Continuing in terminal...

+
+ ``` + + This prevents the user from staring at a resolved choice while the conversation has moved on. When the next visual question comes up, push a new content file as usual. + +6. Repeat until done. + +## Writing Content Fragments + +Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, connection status, and all interactive infrastructure). + +**Minimal example:** + +```html +

Which layout works better?

+

Consider readability and visual hierarchy

+ +
+
+
A
+
+

Single Column

+

Clean, focused reading experience

+
+
+
+
B
+
+

Two Column

+

Sidebar navigation with main content

+
+
+
+``` + +That's it. No ``, no CSS, no ` - -`; -} - -const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8'); -const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8'); -const helperInjection = ''; - -// ========== Helper Functions ========== - -function readSuperpowersVersion() { - const root = path.join(__dirname, '../../..'); - const manifests = [ - path.join(root, 'package.json'), - path.join(root, '.codex-plugin/plugin.json') - ]; - - for (const manifest of manifests) { - try { - const data = JSON.parse(fs.readFileSync(manifest, 'utf-8')); - if (data.version) return String(data.version); - } catch (e) { - // Packaged Codex plugins omit package.json; try the next manifest. - } - } - - return 'unknown'; -} - -function isTruthyEnv(value) { - if (!value) return false; - const normalized = String(value).trim().toLowerCase(); - if (!normalized) return false; - return !['0', 'false', 'no', 'off'].includes(normalized); -} - -function escapeHtmlText(value) { - return String(value) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); -} - -function brandMarkup() { - const version = escapeHtmlText(SUPERPOWERS_VERSION); - const text = SUPERPOWERS_TELEMETRY_DISABLED - ? 'Prime Radiant Superpowers v' + version - : 'Superpowers v' + version; - const logo = SUPERPOWERS_TELEMETRY_DISABLED - ? '' - : ''; - - return ''; -} - -function renderBranding(html) { - return html.split('').join(brandMarkup()); -} - -function isFullDocument(html) { - const trimmed = html.trimStart().toLowerCase(); - return trimmed.startsWith('', content); -} - -function getNewestScreen() { - const files = fs.readdirSync(CONTENT_DIR) - .filter(f => !f.startsWith('.') && f.endsWith('.html')) - .map(f => { - const fp = path.join(CONTENT_DIR, f); - if (!isRegularFileInsideContentDir(fp)) return null; - return { path: fp, mtime: fs.statSync(fp).mtime.getTime() }; - }) - .filter(Boolean) - .sort((a, b) => b.mtime - a.mtime); - return files.length > 0 ? files[0].path : null; -} - -function urlHostForHttp(host) { - const h = String(host); - if (h.startsWith('[') && h.endsWith(']')) return h; - return h.includes(':') ? '[' + h + ']' : h; -} - -function companionUrl() { - return 'http://' + urlHostForHttp(URL_HOST) + ':' + PORT + '/?key=' + TOKEN; -} - -function browserLauncherForPlatform(url, { - platform = process.platform, - osRelease = require('os').release(), - env = process.env -} = {}) { - const isWSL = platform === 'linux' && /microsoft/i.test(osRelease); - if (platform === 'darwin') return { bin: 'open', args: [url] }; - if (platform === 'win32' || isWSL) { - return { bin: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', url] }; - } - if (env.DISPLAY || env.WAYLAND_DISPLAY) return { bin: 'xdg-open', args: [url] }; - return null; -} - -function isRegularFileInsideContentDir(filePath) { - let stat, realContentDir, realFilePath; - try { - stat = fs.lstatSync(filePath); - if (stat.isSymbolicLink()) return false; - if (!stat.isFile()) return false; - if (stat.nlink !== 1) return false; - realContentDir = fs.realpathSync(CONTENT_DIR); - realFilePath = fs.realpathSync(filePath); - } catch (e) { - return false; - } - return realFilePath.startsWith(realContentDir + path.sep); -} - -// ========== Authentication ========== - -function timingSafeEqualStr(a, b) { - const ab = Buffer.from(String(a)); - const bb = Buffer.from(String(b)); - if (ab.length !== bb.length) return false; - return crypto.timingSafeEqual(ab, bb); -} - -function parseCookies(header) { - const out = {}; - if (!header) return out; - for (const part of header.split(';')) { - const eq = part.indexOf('='); - if (eq < 0) continue; - out[part.slice(0, eq).trim()] = part.slice(eq + 1).trim(); - } - return out; -} - -// A request is authorized if it carries the session key as ?key= or as the -// session cookie. Both are compared in constant time. -function isAuthorized(req) { - const q = req.url.indexOf('?'); - if (q >= 0) { - const params = new URLSearchParams(req.url.slice(q + 1)); - if (params.has('key')) { - const key = params.get('key'); - return Boolean(key && timingSafeEqualStr(key, TOKEN)); - } - } - const cookie = parseCookies(req.headers['cookie'])[COOKIE_NAME]; - if (cookie && timingSafeEqualStr(cookie, TOKEN)) return true; - return false; -} - -function pathnameOf(url) { - const q = url.indexOf('?'); - return q >= 0 ? url.slice(0, q) : url; -} - -function queryKey(url) { - const q = url.indexOf('?'); - if (q < 0) return null; - return new URLSearchParams(url.slice(q + 1)).get('key'); -} - -function securityHeaders(headers = {}) { - return { - 'Referrer-Policy': 'no-referrer', - 'Cache-Control': 'no-store', - 'X-Frame-Options': 'DENY', - 'Content-Security-Policy': "frame-ancestors 'none'", - 'Cross-Origin-Resource-Policy': 'same-origin', - ...headers - }; -} - -function isAllowedWebSocketOrigin(req) { - const origin = req.headers.origin; - if (!origin) return true; - const host = req.headers.host; - if (!host) return false; - return origin === 'http://' + host; -} - -// ========== HTTP Request Handler ========== - -function handleRequest(req, res) { - if (!isAuthorized(req)) { - res.writeHead(403, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' })); - res.end(FORBIDDEN_PAGE); - return; - } - touchActivity(); // only authorized requests count as activity - - // Mirror the key into a cookie so same-origin subresources (/files/*) can - // authenticate after bootstrap. HttpOnly keeps it away from page scripts; the - // WebSocket Origin check below is what blocks cross-origin localhost injection. - res.setHeader('Set-Cookie', - COOKIE_NAME + '=' + TOKEN + '; HttpOnly; SameSite=Strict; Path=/'); - - const pathname = pathnameOf(req.url); - const keyFromQuery = queryKey(req.url); - if (req.method === 'GET' && pathname === '/' && keyFromQuery && timingSafeEqualStr(keyFromQuery, TOKEN)) { - res.writeHead(200, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' })); - res.end(bootstrapPage(keyFromQuery)); - } else if (req.method === 'GET' && pathname === '/') { - const screenFile = getNewestScreen(); - let html = screenFile - ? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8')) - : waitingPage(); - - if (html.includes('')) { - html = html.replace('', helperInjection + '\n'); - } else { - html += helperInjection; - } - - res.writeHead(200, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' })); - res.end(html); - } else if (req.method === 'GET' && pathname.startsWith('/files/')) { - const fileName = path.basename(pathname.slice(7)); - const filePath = path.join(CONTENT_DIR, fileName); - // Reject empty/dotfile names and anything that isn't a regular file โ€” - // `/files/` would otherwise resolve to CONTENT_DIR and crash readFileSync (EISDIR). - if (!fileName || fileName.startsWith('.') || !isRegularFileInsideContentDir(filePath)) { - res.writeHead(404, securityHeaders()); - res.end('Not found'); - return; - } - const ext = path.extname(filePath).toLowerCase(); - const contentType = MIME_TYPES[ext] || 'application/octet-stream'; - res.writeHead(200, securityHeaders({ 'Content-Type': contentType })); - res.end(fs.readFileSync(filePath)); - } else { - res.writeHead(404, securityHeaders()); - res.end('Not found'); - } -} - -// ========== WebSocket Connection Handling ========== - -const clients = new Set(); - -function handleUpgrade(req, socket) { - if (!isAuthorized(req) || !isAllowedWebSocketOrigin(req)) { socket.destroy(); return; } - - const key = req.headers['sec-websocket-key']; - if (!key) { socket.destroy(); return; } - - const accept = computeAcceptKey(key); - socket.write( - 'HTTP/1.1 101 Switching Protocols\r\n' + - 'Upgrade: websocket\r\n' + - 'Connection: Upgrade\r\n' + - 'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n' - ); - - let buffer = Buffer.alloc(0); - clients.add(socket); - - socket.on('data', (chunk) => { - buffer = Buffer.concat([buffer, chunk]); - while (buffer.length > 0) { - let result; - try { - result = decodeFrame(buffer); - } catch (e) { - socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0))); - clients.delete(socket); - return; - } - if (!result) break; - buffer = buffer.slice(result.bytesConsumed); - - switch (result.opcode) { - case OPCODES.TEXT: - handleMessage(result.payload.toString()); - break; - case OPCODES.CLOSE: - socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0))); - clients.delete(socket); - return; - case OPCODES.PING: - socket.write(encodeFrame(OPCODES.PONG, result.payload)); - break; - case OPCODES.PONG: - break; - default: { - const closeBuf = Buffer.alloc(2); - closeBuf.writeUInt16BE(1003); - socket.end(encodeFrame(OPCODES.CLOSE, closeBuf)); - clients.delete(socket); - return; - } - } - } - }); - - socket.on('close', () => clients.delete(socket)); - socket.on('error', () => clients.delete(socket)); -} - -function handleMessage(text) { - let event; - try { - event = JSON.parse(text); - } catch (e) { - console.error('Failed to parse WebSocket message:', e.message); - return; - } - touchActivity(); - console.log(JSON.stringify({ source: 'user-event', ...event })); - if (event && event.choice) { - const eventsFile = path.join(STATE_DIR, 'events'); - fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n'); - } -} - -function broadcast(msg) { - const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg))); - for (const socket of clients) { - try { socket.write(frame); } catch (e) { clients.delete(socket); } - } -} - -// Best-effort: open the user's browser the first time a screen is actually ready -// to show. Skips when disabled, on a non-loopback (remote) bind, or when a -// browser is already connected. Override the launcher with BRAINSTORM_OPEN_CMD. -let browserOpened = false; -function maybeOpenBrowser() { - if (browserOpened) return; - browserOpened = true; - if (!process.env.BRAINSTORM_OPEN) return; // opt-in: only after the user approves the companion - if (HOST !== '127.0.0.1' && HOST !== 'localhost') return; - if (clients.size > 0) return; // the user already opened it - const url = companionUrl(); // must carry the key or the gate 403s it - const cp = require('child_process'); - // Operator-provided launcher: run as given (this env var is trusted operator input). - if (process.env.BRAINSTORM_OPEN_CMD) { - try { cp.exec(process.env.BRAINSTORM_OPEN_CMD + ' ' + JSON.stringify(url), () => {}); } catch (e) { /* best effort */ } - return; - } - // Platform launchers: pass the URL as an argv element via execFile (no shell), - // so a url-host containing shell metacharacters can't inject a command. - const launcher = browserLauncherForPlatform(url); - if (!launcher) return; // headless: nothing to open - try { cp.execFile(launcher.bin, launcher.args, () => {}); } catch (e) { /* best effort */ } -} - -// ========== Activity Tracking ========== - -// Idle timeout: shut down after this long with no activity. Default 4 hours; -// override with BRAINSTORM_IDLE_TIMEOUT_MS (start-server.sh: --idle-timeout-minutes). -const IDLE_TIMEOUT_MS = (() => { - const ms = Number(process.env.BRAINSTORM_IDLE_TIMEOUT_MS); - return Number.isFinite(ms) && ms > 0 ? ms : 4 * 60 * 60 * 1000; -})(); -// How often the watchdog checks for owner-death / idleness. Configurable mainly -// so tests can run fast; production default is 60s. -const LIFECYCLE_CHECK_MS = (() => { - const ms = Number(process.env.BRAINSTORM_LIFECYCLE_CHECK_MS); - return Number.isFinite(ms) && ms > 0 ? ms : 60 * 1000; -})(); -let lastActivity = Date.now(); - -function touchActivity() { - lastActivity = Date.now(); -} - -// ========== File Watching ========== - -const debounceTimers = new Map(); - -// ========== Server Startup ========== - -function startServer() { - if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true }); - if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); - - // Track known files to distinguish new screens from updates. - // macOS fs.watch reports 'rename' for both new files and overwrites, - // so we can't rely on eventType alone. - const knownFiles = new Set( - fs.readdirSync(CONTENT_DIR).filter(f => !f.startsWith('.') && f.endsWith('.html')) - ); - - const server = http.createServer(handleRequest); - server.on('upgrade', handleUpgrade); - - const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => { - if (!filename || filename.startsWith('.') || !filename.endsWith('.html')) return; - - if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename)); - debounceTimers.set(filename, setTimeout(() => { - debounceTimers.delete(filename); - const filePath = path.join(CONTENT_DIR, filename); - - if (!fs.existsSync(filePath)) return; // file was deleted - touchActivity(); - - if (!knownFiles.has(filename)) { - knownFiles.add(filename); - const eventsFile = path.join(STATE_DIR, 'events'); - if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); - console.log(JSON.stringify({ type: 'screen-added', file: filePath })); - maybeOpenBrowser(); - } else { - console.log(JSON.stringify({ type: 'screen-updated', file: filePath })); - } - - broadcast({ type: 'reload' }); - }, 100)); - }); - watcher.on('error', (err) => console.error('fs.watch error:', err.message)); - - function shutdown(reason) { - console.log(JSON.stringify({ type: 'server-stopped', reason })); - const infoFile = path.join(STATE_DIR, 'server-info'); - if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile); - fs.writeFileSync( - path.join(STATE_DIR, 'server-stopped'), - JSON.stringify({ reason, timestamp: Date.now() }) + '\n' - ); - watcher.close(); - clearInterval(lifecycleCheck); - // Close any upgraded WebSocket sockets so server.close() can complete and - // the process actually exits instead of lingering on an open connection. - for (const socket of clients) { - try { socket.destroy(); } catch (e) { /* already gone */ } - } - server.close(() => process.exit(0)); - } - - function ownerAlive() { - if (!ownerPid) return true; - try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; } - } - - // Periodically exit if the owner process died or we've been idle too long. - const lifecycleCheck = setInterval(() => { - if (!ownerAlive()) shutdown('owner process exited'); - else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown('idle timeout'); - }, LIFECYCLE_CHECK_MS); - lifecycleCheck.unref(); - - // Validate owner PID at startup. If it's already dead, the PID resolution - // was wrong (common on WSL, Tailscale SSH, and cross-user scenarios). - // Disable monitoring and rely on the idle timeout instead. - if (ownerPid) { - try { process.kill(ownerPid, 0); } - catch (e) { - if (e.code !== 'EPERM') { - console.log(JSON.stringify({ type: 'owner-pid-invalid', pid: ownerPid, reason: 'dead at startup' })); - ownerPid = null; - } - } - } - - // If the preferred port is already taken (e.g. a previous server is still - // alive), fall back to a random port once instead of failing. - let triedFallback = false; - - function onListen() { - // Cookie name keys on the ACTUAL bound port (may differ from the preferred - // one after an EADDRINUSE fallback) so it can't collide with another server's - // cookie in the shared localhost jar. - COOKIE_NAME = 'brainstorm-key-' + PORT; - // Record the bound port AND token so the next restart of this session reuses - // them โ€” but ONLY when we got our preferred port. On a fallback we bound a - // *different* port because someone else holds the preferred one; persisting - // would overwrite the shared files and strand that other session's open tab. - if (PORT_FILE && !triedFallback) { - try { fs.writeFileSync(PORT_FILE, String(PORT)); } catch (e) { /* best effort */ } - if (TOKEN_FILE) { - try { - fs.writeFileSync(TOKEN_FILE, TOKEN, { mode: 0o600 }); - chmodOwnerOnly(TOKEN_FILE); - } catch (e) { /* best effort */ } - } - } - const info = JSON.stringify({ - type: 'server-started', port: Number(PORT), host: HOST, - url_host: URL_HOST, url: companionUrl(), - screen_dir: CONTENT_DIR, state_dir: STATE_DIR, idle_timeout_ms: IDLE_TIMEOUT_MS - }); - console.log(info); - // server-info embeds the key โ€” keep it owner-only. - fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n', { mode: 0o600 }); - } - - server.on('error', (err) => { - if (err.code === 'EADDRINUSE' && !triedFallback) { - if (tokenSource === 'env') { - console.error('Server failed to bind: preferred port is in use and BRAINSTORM_TOKEN is set; refusing fallback with explicit token'); - process.exit(1); - } - triedFallback = true; - PORT = randomPort(); - if (tokenSource === 'file') { - TOKEN = generateToken(); - tokenSource = 'generated-fallback'; - } - server.listen(PORT, HOST, onListen); - } else { - console.error('Server failed to bind:', err.message); - process.exit(1); - } - }); - server.listen(PORT, HOST, onListen); -} - -if (require.main === module) { - startServer(); -} - -module.exports = { - computeAcceptKey, - encodeFrame, - decodeFrame, - browserLauncherForPlatform, - OPCODES, - MAX_FRAME_PAYLOAD_BYTES -}; diff --git a/plugins/techletes-superpowers/skills/brainstorming/scripts/start-server.sh b/plugins/techletes-superpowers/skills/brainstorming/scripts/start-server.sh deleted file mode 100755 index 016a8e48..00000000 --- a/plugins/techletes-superpowers/skills/brainstorming/scripts/start-server.sh +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env bash -# Start the brainstorm server and output connection info -# Usage: start-server.sh [--project-dir ] [--host ] [--url-host ] [--foreground] [--background] -# -# Starts server on a random high port, outputs JSON with URL. -# Each session gets its own directory to avoid conflicts. -# -# Options: -# --project-dir Store session files under /.superpowers/brainstorm/ -# instead of /tmp. Files persist after server stops. -# --host Host/interface to bind (default: 127.0.0.1). -# Use 0.0.0.0 in remote/containerized environments. -# --url-host Hostname shown in returned URL JSON. -# --idle-timeout-minutes Shut down after n minutes idle (default 240 = 4h). -# --open Auto-open the browser on the first screen (use only -# after the user approves the visual companion). -# --foreground Run server in the current terminal (no backgrounding). -# --background Force background mode (overrides Codex auto-foreground). - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -# Parse arguments -PROJECT_DIR="" -FOREGROUND="false" -FORCE_BACKGROUND="false" -BIND_HOST="127.0.0.1" -URL_HOST="" -IDLE_TIMEOUT_MINUTES="" -while [[ $# -gt 0 ]]; do - case "$1" in - --project-dir) - PROJECT_DIR="$2" - shift 2 - ;; - --host) - BIND_HOST="$2" - shift 2 - ;; - --url-host) - URL_HOST="$2" - shift 2 - ;; - --idle-timeout-minutes) - IDLE_TIMEOUT_MINUTES="$2" - shift 2 - ;; - --open) - export BRAINSTORM_OPEN=1 - shift - ;; - --foreground|--no-daemon) - FOREGROUND="true" - shift - ;; - --background|--daemon) - FORCE_BACKGROUND="true" - shift - ;; - *) - echo "{\"error\": \"Unknown argument: $1\"}" - exit 1 - ;; - esac -done - -if [[ -z "$URL_HOST" ]]; then - if [[ "$BIND_HOST" == "127.0.0.1" || "$BIND_HOST" == "localhost" ]]; then - URL_HOST="localhost" - else - URL_HOST="$BIND_HOST" - fi -fi - -if [[ -n "$IDLE_TIMEOUT_MINUTES" ]]; then - if ! [[ "$IDLE_TIMEOUT_MINUTES" =~ ^[0-9]+$ ]] || [[ "$IDLE_TIMEOUT_MINUTES" -lt 1 ]]; then - echo "{\"error\": \"--idle-timeout-minutes must be a positive integer\"}" - exit 1 - fi - export BRAINSTORM_IDLE_TIMEOUT_MS=$(( IDLE_TIMEOUT_MINUTES * 60 * 1000 )) -fi - -is_windows_like_shell() { - case "${OSTYPE:-}" in - msys*|cygwin*|mingw*) return 0 ;; - esac - if [[ -n "${MSYSTEM:-}" ]]; then - return 0 - fi - local uname_s - uname_s="$(uname -s 2>/dev/null || true)" - case "$uname_s" in - MSYS*|MINGW*|CYGWIN*) return 0 ;; - esac - return 1 -} - -# Some environments reap detached/background processes. Auto-foreground when detected. -if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then - FOREGROUND="true" -fi - -# Windows/Git Bash reaps nohup background processes. Auto-foreground when detected. -if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then - if is_windows_like_shell; then - FOREGROUND="true" - fi -fi - -# Session files (server.log, server-info, .last-token) embed the session key โ€” -# keep everything this script and the server create owner-only. -umask 077 - -# Generate unique session directory -SESSION_ID="$$-$(date +%s)" - -if [[ -n "$PROJECT_DIR" ]]; then - SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}" - # Persist the bound port and key per project so a restart reuses them and an - # already-open browser tab reconnects to the same URL with a valid cookie. - export BRAINSTORM_PORT_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-port" - export BRAINSTORM_TOKEN_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-token" -else - SESSION_DIR="/tmp/brainstorm-${SESSION_ID}" -fi - -STATE_DIR="${SESSION_DIR}/state" -PID_FILE="${STATE_DIR}/server.pid" -LOG_FILE="${STATE_DIR}/server.log" -SERVER_ID_FILE="${STATE_DIR}/server-instance-id" - -# Create fresh session directory with content and state peers -mkdir -p "${SESSION_DIR}/content" "$STATE_DIR" - -SERVER_ID="" -if [[ -r /dev/urandom ]]; then - SERVER_ID="$(od -An -N24 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' || true)" -fi -if ! [[ "$SERVER_ID" =~ ^[A-Za-z0-9_-]{32,64}$ ]]; then - SERVER_ID="$(printf '%08x%08x%08x%08x' "$$" "$(date +%s)" "${RANDOM:-0}" "${RANDOM:-0}")" -fi -printf '%s\n' "$SERVER_ID" > "$SERVER_ID_FILE" -chmod 600 "$SERVER_ID_FILE" 2>/dev/null || true - -# Kill any existing server -if [[ -f "$PID_FILE" ]]; then - old_pid=$(cat "$PID_FILE") - kill "$old_pid" 2>/dev/null - rm -f "$PID_FILE" -fi - -cd "$SCRIPT_DIR" || exit 1 - -# Resolve the harness PID (grandparent of this script). -# $PPID is the ephemeral shell the harness spawned to run us โ€” it dies -# when this script exits. The harness itself is $PPID's parent. -OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')" -if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then - OWNER_PID="$PPID" -fi - -# Windows/MSYS2: Node.js cannot see POSIX PIDs from the MSYS2 namespace. -# Passing a PID node cannot verify causes server to log owner-pid-invalid -# and self-terminate at the 60-second lifecycle check. Clear it so the -# watchdog is disabled and the idle timeout becomes the only shutdown trigger. -if is_windows_like_shell; then - OWNER_PID="" -fi - -# Foreground mode for environments that reap detached/background processes. -if [[ "$FOREGROUND" == "true" ]]; then - env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs "--brainstorm-server-id=$SERVER_ID" & - SERVER_PID=$! - echo "$SERVER_PID" > "$PID_FILE" - wait "$SERVER_PID" - exit $? -fi - -# Start server, capturing output to log file -# Use nohup to survive shell exit; disown to remove from job table -nohup env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs "--brainstorm-server-id=$SERVER_ID" > "$LOG_FILE" 2>&1 & -SERVER_PID=$! -disown "$SERVER_PID" 2>/dev/null -echo "$SERVER_PID" > "$PID_FILE" - -# Wait for server-started message (check log file) -for _ in {1..50}; do - if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then - # Verify server is still alive after a short window (catches process reapers) - alive="true" - for _ in {1..20}; do - if ! kill -0 "$SERVER_PID" 2>/dev/null; then - alive="false" - break - fi - sleep 0.1 - done - if [[ "$alive" != "true" ]]; then - echo "{\"error\": \"Server started but was killed. Retry in a persistent terminal with: $SCRIPT_DIR/start-server.sh${PROJECT_DIR:+ --project-dir $PROJECT_DIR} --host $BIND_HOST --url-host $URL_HOST --foreground\"}" - exit 1 - fi - grep "server-started" "$LOG_FILE" | head -1 - exit 0 - fi - sleep 0.1 -done - -# Timeout - server didn't start -echo '{"error": "Server failed to start within 5 seconds"}' -exit 1 diff --git a/plugins/techletes-superpowers/skills/brainstorming/scripts/stop-server.sh b/plugins/techletes-superpowers/skills/brainstorming/scripts/stop-server.sh deleted file mode 100755 index 7cacfe94..00000000 --- a/plugins/techletes-superpowers/skills/brainstorming/scripts/stop-server.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env bash -# Stop the brainstorm server and clean up -# Usage: stop-server.sh -# -# Kills the server process. Only deletes session directory if it's -# under /tmp (ephemeral). Persistent directories (.superpowers/) are -# kept so mockups can be reviewed later. - -SESSION_DIR="$1" - -if [[ -z "$SESSION_DIR" ]]; then - echo '{"error": "Usage: stop-server.sh "}' - exit 1 -fi - -STATE_DIR="${SESSION_DIR}/state" -PID_FILE="${STATE_DIR}/server.pid" -SERVER_ID_FILE="${STATE_DIR}/server-instance-id" - -mark_stopped() { - local reason="$1" - rm -f "${STATE_DIR}/server-info" - printf '{"reason":"%s","timestamp":%s}\n' "$reason" "$(date +%s)" > "${STATE_DIR}/server-stopped" -} - -read_expected_server_id() { - [[ -f "$SERVER_ID_FILE" ]] || return 1 - local id - id="$(tr -d '\r\n' < "$SERVER_ID_FILE" 2>/dev/null || true)" - [[ "$id" =~ ^[A-Za-z0-9_-]{32,64}$ ]] || return 1 - printf '%s\n' "$id" -} - -command_line_for_pid() { - local pid="$1" - if [[ -r "/proc/$pid/cmdline" ]]; then - tr '\0' '\n' < "/proc/$pid/cmdline" 2>/dev/null || true - return 0 - fi - ps -ww -p "$pid" -o command= 2>/dev/null || ps -f -p "$pid" 2>/dev/null | sed '1d' || true -} - -command_has_server_id() { - local pid="$1" - local expected="$2" - local expected_arg="--brainstorm-server-id=$expected" - if [[ -r "/proc/$pid/cmdline" ]]; then - local arg - while IFS= read -r -d '' arg || [[ -n "$arg" ]]; do - [[ "$arg" == "$expected_arg" ]] && return 0 - done < "/proc/$pid/cmdline" - return 1 - fi - local command_line - command_line="$(command_line_for_pid "$pid")" - [[ -n "$command_line" ]] || return 1 - case " $command_line " in - *" $expected_arg "*) return 0 ;; - *) return 1 ;; - esac -} - -# Confirm a PID has this session's per-start instance id, not just a familiar -# process name. Ambiguous or legacy metadata fails closed as stale_pid. -is_brainstorm_server() { - kill -0 "$1" 2>/dev/null || return 1 - local expected_id - expected_id="$(read_expected_server_id)" || return 1 - command_has_server_id "$1" "$expected_id" || return 1 - return 0 -} - -if [[ -f "$PID_FILE" ]]; then - pid=$(cat "$PID_FILE") - - # Refuse to signal a PID we can't prove is our server. A stale pid file may - # point at an unrelated process after a reboot/PID wraparound. - if ! is_brainstorm_server "$pid"; then - rm -f "$PID_FILE" "$SERVER_ID_FILE" - mark_stopped "stale_pid" - echo '{"status": "stale_pid"}' - exit 0 - fi - - # Try to stop gracefully, fallback to force if still alive - kill "$pid" 2>/dev/null || true - - # Wait for graceful shutdown (up to ~2s) - for _ in {1..20}; do - if ! kill -0 "$pid" 2>/dev/null; then - break - fi - sleep 0.1 - done - - # If still running, escalate to SIGKILL - if kill -0 "$pid" 2>/dev/null; then - kill -9 "$pid" 2>/dev/null || true - - # Give SIGKILL a moment to take effect - sleep 0.1 - fi - - if kill -0 "$pid" 2>/dev/null; then - echo '{"status": "failed", "error": "process still running"}' - exit 1 - fi - - rm -f "$PID_FILE" "$SERVER_ID_FILE" "${STATE_DIR}/server.log" - mark_stopped "stop-server.sh" - - # Only delete ephemeral /tmp directories - if [[ "$SESSION_DIR" == /tmp/* ]]; then - rm -rf "$SESSION_DIR" - fi - - echo '{"status": "stopped"}' -else - echo '{"status": "not_running"}' -fi diff --git a/plugins/techletes-superpowers/skills/brainstorming/spec-document-reviewer-prompt.md b/plugins/techletes-superpowers/skills/brainstorming/spec-document-reviewer-prompt.md deleted file mode 100644 index 60993129..00000000 --- a/plugins/techletes-superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +++ /dev/null @@ -1,49 +0,0 @@ -# Spec Document Reviewer Prompt Template - -Use this template when dispatching a spec document reviewer subagent. - -**Purpose:** Verify the spec is complete, consistent, and ready for implementation planning. - -**Dispatch after:** Spec document is written to docs/superpowers/specs/ - -``` -Subagent (general-purpose): - description: "Review spec document" - prompt: | - You are a spec document reviewer. Verify this spec is complete and ready for planning. - - **Spec to review:** [SPEC_FILE_PATH] - - ## What to Check - - | Category | What to Look For | - |----------|------------------| - | Completeness | TODOs, placeholders, "TBD", incomplete sections | - | Consistency | Internal contradictions, conflicting requirements | - | Clarity | Requirements ambiguous enough to cause someone to build the wrong thing | - | Scope | Focused enough for a single plan โ€” not covering multiple independent subsystems | - | YAGNI | Unrequested features, over-engineering | - - ## Calibration - - **Only flag issues that would cause real problems during implementation planning.** - A missing section, a contradiction, or a requirement so ambiguous it could be - interpreted two different ways โ€” those are issues. Minor wording improvements, - stylistic preferences, and "sections less detailed than others" are not. - - Approve unless there are serious gaps that would lead to a flawed plan. - - ## Output Format - - ## Spec Review - - **Status:** Approved | Issues Found - - **Issues (if any):** - - [Section X]: [specific issue] - [why it matters for planning] - - **Recommendations (advisory, do not block approval):** - - [suggestions for improvement] -``` - -**Reviewer returns:** Status, Issues (if any), Recommendations diff --git a/plugins/techletes-superpowers/skills/brainstorming/visual-companion.md b/plugins/techletes-superpowers/skills/brainstorming/visual-companion.md deleted file mode 100644 index 7b89f6b2..00000000 --- a/plugins/techletes-superpowers/skills/brainstorming/visual-companion.md +++ /dev/null @@ -1,291 +0,0 @@ -# Visual Companion Guide - -Browser-based visual brainstorming companion for showing mockups, diagrams, and options. - -## When to Use - -Decide per-question, not per-session. The test: **would the user understand this better by seeing it than reading it?** - -**Use the browser** when the content itself is visual: - -- **UI mockups** โ€” wireframes, layouts, navigation structures, component designs -- **Architecture diagrams** โ€” system components, data flow, relationship maps -- **Side-by-side visual comparisons** โ€” comparing two layouts, two color schemes, two design directions -- **Design polish** โ€” when the question is about look and feel, spacing, visual hierarchy -- **Spatial relationships** โ€” state machines, flowcharts, entity relationships rendered as diagrams - -**Use the terminal** when the content is text or tabular: - -- **Requirements and scope questions** โ€” "what does X mean?", "which features are in scope?" -- **Conceptual A/B/C choices** โ€” picking between approaches described in words -- **Tradeoff lists** โ€” pros/cons, comparison tables -- **Technical decisions** โ€” API design, data modeling, architectural approach selection -- **Clarifying questions** โ€” anything where the answer is words, not a visual preference - -A question *about* a UI topic is not automatically a visual question. "What kind of wizard do you want?" is conceptual โ€” use the terminal. "Which of these wizard layouts feels right?" is visual โ€” use the browser. - -## How It Works - -The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn. - -**Content fragments vs full documents:** If your HTML file starts with `/.superpowers/brainstorm/` for the session directory. - -**Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there. - -**Launching the server by platform:** - -**Claude Code:** -```bash -# Default mode works โ€” the script backgrounds the server itself. -scripts/start-server.sh --project-dir /path/to/project --open -``` - -On Windows, the script auto-detects and switches to foreground mode (which blocks the tool call). Use `run_in_background: true` on the Bash tool call so the server survives across conversation turns, then read `$STATE_DIR/server-info` on the next turn to get the URL and port. - -**Codex:** -```bash -# Codex reaps background processes. The script auto-detects CODEX_CI and -# switches to foreground mode. Run it normally โ€” no extra flags needed. -scripts/start-server.sh --project-dir /path/to/project --open -``` - -**Copilot CLI:** -```bash -# Use --foreground and start the server via the bash tool with mode: "async" -# so the process survives across turns. Capture the returned shellId for -# read_bash / stop_bash if you need to interact with it later. -scripts/start-server.sh --project-dir /path/to/project --open --foreground -``` - -**Other environments:** The server must keep running in the background across conversation turns. If your environment reaps detached processes, use `--foreground` and launch the command with your platform's background execution mechanism. - -If the URL is unreachable from your browser (common in remote/containerized setups), bind a non-loopback host: - -```bash -scripts/start-server.sh \ - --project-dir /path/to/project \ - --host 0.0.0.0 \ - --url-host localhost -``` - -Use `--url-host` to control what hostname is printed in the returned URL JSON. - -## The Loop - -1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`: - - **Required: confirm the server is alive before referring to the URL or pushing a screen.** Check that `$STATE_DIR/server-info` exists and `$STATE_DIR/server-stopped` does not. If it has shut down, restart it with `start-server.sh` using the **same `--project-dir`** โ€” it reuses the same port, so the user's open tab reconnects on its own (it shows a "paused" overlay while the server is down) and you don't need to send a new URL. The server auto-exits after 4 hours idle (configurable with `--idle-timeout-minutes`). - - Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html` - - **Never reuse filenames** โ€” each screen gets a fresh file - - Use your file-creation tool โ€” **never use cat/heredoc** (dumps noise into terminal) - - Server automatically serves the newest file - -2. **Tell user what to expect and end your turn:** - - Remind them of the URL (every step, not just first) - - Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage") - - Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like." - -3. **On your next turn** โ€” after the user responds in the terminal: - - Read `$STATE_DIR/events` if it exists โ€” this contains the user's browser interactions (clicks, selections) as JSON lines - - Merge with the user's terminal text to get the full picture - - The terminal message is the primary feedback; `state_dir/events` provides structured interaction data - -4. **Iterate or advance** โ€” if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated. - -5. **Unload when returning to terminal** โ€” when the next step doesn't need the browser (e.g., a clarifying question, a tradeoff discussion), push a waiting screen to clear the stale content: - - ```html - -
-

Continuing in terminal...

-
- ``` - - This prevents the user from staring at a resolved choice while the conversation has moved on. When the next visual question comes up, push a new content file as usual. - -6. Repeat until done. - -## Writing Content Fragments - -Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, connection status, and all interactive infrastructure). - -**Minimal example:** - -```html -

Which layout works better?

-

Consider readability and visual hierarchy

- -
-
-
A
-
-

Single Column

-

Clean, focused reading experience

-
-
-
-
B
-
-

Two Column

-

Sidebar navigation with main content

-
-
-
-``` - -That's it. No ``, no CSS, no `