diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index 867f622..0000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1,275 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Repository Overview - -Personal dotfiles repository using Ansible for automated system configuration on macOS and Debian Linux systems. -Manages dotfiles, applications, system settings, and home infrastructure (NAS with media server stack). - -## Architecture - -### Dual Bootstrap System - -Two separate bootstrap workflows targeting different environments: - -- **`local_bootstrap.sh`** → `playbooks/local_bootstrap.yml`: Personal Mac configuration -- **`nas_bootstrap.sh`** → `playbooks/nas_bootstrap.yml`: Home NAS/server configuration - -Both scripts: -1. Validate repository structure -2. Install Homebrew with checksum verification/ -3. Install Ansible via Homebrew -4. Execute corresponding Ansible playbook - -### Inventory Management - -- **Primary inventory**: `inventory/` (standalone git clone, managed by run.py) -- **Repository**: Cloned from `git@github.com:connormason/dotfiles-inventory.git` -- **Contains**: Host definitions, encrypted vault files for secrets -- **Setup command**: `python3 run.py update-inventory` (clones if missing, pulls if exists) -- **Recovery**: `python3 run.py update-inventory --force` (removes and re-clones if corrupted) -- **Vault password**: Stored in `vault_password.txt` (gitignored, must be created manually) - -**Note**: The inventory is NOT a git submodule. It is a separate git repository cloned into the `inventory/` directory by `run.py`. This simplifies the mental model for personal dotfiles while maintaining version control. The directory is excluded from the main repository via `.gitignore`. - -Inventory structure: -``` -inventory/ -├── inventory.yml # Host definitions -├── group_vars/ -│ ├── all/vault.yml # Shared secrets -│ └── localhost/ -│ ├── vars.yml # Mac-specific variables -│ └── vault.yml # Mac-specific secrets -└── host_vars/ - └── nas/ - ├── vars.yml # NAS-specific variables - └── vault.yml # NAS-specific secrets -``` - -### Role System - -Ansible roles organized by target system: - -**macOS roles**: -- `macos`: Homebrew packages, cask apps, Mac App Store apps -- `macos_settings`: System preferences (Finder, Dock, Activity Monitor, etc.) -- `macos_dock`: Dock configuration -- `hammerspoon`: Window management automation -- `iterm`: iTerm2 configuration -- `python`: Python tooling (pipx, uv, hatch) -- `starship`: Shell prompt configuration - -**Linux roles**: -- `debian`: System packages and configuration -- `docker`: Docker setup with compose stack (Plex, Sonarr, Radarr, PiHole, Home Assistant, etc.) -- `zfs`: ZFS filesystem configuration -- `samba`: File sharing setup - -**Shared roles**: -- `git`: Git configuration and gh CLI -- `ssh`: SSH client configuration -- `zsh`: Shell configuration -- `link_dotfile`: Reusable role for symlinking dotfiles - -### link_dotfile Role Pattern - -Reusable role for safely creating dotfile symlinks: -- Validates source exists -- Creates parent directories -- Backs up existing non-symlink files (with timestamp) -- Only updates if symlink missing or pointing to wrong target - -Usage in playbooks: -```yaml -- include_role: - name: roles/link_dotfile - vars: - link_dotfile_src: "{{ dotfiles_dir }}/path/to/source" - link_dotfile_dst: "{{ home_dir }}/.config/destination" -``` - -### Python Management Script - -`run.py` provides CLI interface to repository operations: - -**Command categories**: -- **Inventory**: `list-hosts`, `update-inventory` -- **Tool Installation**: `install-uv`, `install-hatch` -- **Codebase**: `clean`, `pre`, `makefile` - -**Key features**: -- Type-annotated with full typing support -- ANSI styling via custom `style()` function -- Command registration via `@command` decorator -- Auto-generates Makefile from registered commands -- Retry logic with exponential backoff for network operations - -**Environment variables**: -- `DOTFILES_RUN_DEBUG`: Enable debug output -- `DOTFILES_INVENTORY_REPO_URL`: Override inventory repo URL - -## Development Commands - -### Running Ansible Playbooks - -```bash -# Bootstrap local Mac (interactive, asks for sudo password) -chmod u+x local_bootstrap.sh && ./local_bootstrap.sh - -# Bootstrap NAS server -chmod u+x nas_bootstrap.sh && ./nas_bootstrap.sh - -# Run specific tags only -ansible-playbook playbooks/local_bootstrap.yml -i inventory/inventory.yml --ask-become-pass -vv --tags git,zsh - -# Run with extra variables -ansible-playbook playbooks/local_bootstrap.yml -i inventory/inventory.yml --ask-become-pass -e "some_var=value" -``` - -### Python Script Commands - -```bash -# List available inventory hosts -python3 run.py list-hosts - -# Update inventory from remote repository -python3 run.py update-inventory - -# Clean build artifacts and caches -python3 run.py clean - -# Run pre-commit hooks on all files -python3 run.py pre - -# Install Python tooling -python3 run.py install-uv -python3 run.py install-hatch - -# Generate Makefile from run.py commands -python3 run.py makefile -``` - -### Using Make (auto-generated) - -```bash -# Show available targets -make help - -# All python3 run.py commands available as make targets -make update-inventory -make clean -make pre -``` - -## Testing and Validation - -### Pre-commit Hooks - -Configured in `.pre-commit-config.yaml`: - -- **File integrity**: Large files, merge conflicts, private keys, symlinks -- **Python**: AST validation, debug statements, ruff linting, mypy type checking, interrogate docstring coverage -- **Data formats**: JSON, YAML, TOML, XML validation -- **YAML**: yamllint with custom config (`.yamllint.yaml`) -- **Fixers**: Whitespace, line endings, UTF-8 BOM - -Run hooks: -```bash -# All files -pre-commit run --all-files - -# Or via script -python3 run.py pre -``` - -### Ansible Linting - -Commented out in pre-commit config but available: -- `ansible-lint` with `.ansible-lint.yaml` configuration -- `shellcheck` for shell script validation - -## Key Configuration Files - -- **`.pre-commit-config.yaml`**: Code quality hooks -- **`.yamllint.yaml`**: YAML linting rules -- **`.ansible-lint.yaml`**: Ansible best practices -- **`vault_password.txt`**: Ansible Vault password (gitignored, create manually) -- **`roles/requirements.yml`**: External Ansible role dependencies - -## Security Considerations - -### Vault Management - -- All secrets stored in Ansible Vault encrypted files -- Vault password required in `vault_password.txt` for playbook execution -- Never commit vault password or decrypted secrets - -### Bootstrap Script Security - -- Homebrew installer checksum verified before execution (local_bootstrap.sh:41) -- Checksum expected value: `b2ffbf7e7f451c6db3b5d1976fc6a9c2faecf58ee5e1dbf6e498643c91f0d3bc` -- Update checksum when Homebrew installer changes: -`curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | shasum -a 256` - -## Docker Stack (NAS) - -Media server and home automation stack in `roles/docker/files/docker-compose.yml`: - -**Services**: -- **Media**: Plex, Jellyfin, Radarr (movies), Sonarr (TV), Transmission (torrents), Prowlarr (indexer), Flaresolverr -- **Network**: PiHole (DNS/ad-blocking) -- **Automation**: Home Assistant, Glance dashboard -- **Support**: Autoplex (automated file organization) - -**Storage paths**: `/storage/media/*` mounted into containers - -## macOS-Specific - -### Package Management Layers - -1. **Homebrew formulae** (`brew_packages` in `roles/macos/defaults/main.yml`): CLI tools -2. **Homebrew casks** (`brew_cask_packages`): GUI applications -3. **Mac App Store** (`mas_apps`): App Store apps via `mas` CLI - -### System Settings - -The `macos_settings` role configures system preferences via `defaults write` commands: -- Finder behavior and appearance -- Dock size/position/behavior -- Activity Monitor preferences -- Messages app settings -- Power management -- I/O devices (keyboard, trackpad) - -Applied via separate task files in `roles/macos_settings/tasks/`. - -## Common Patterns - -### Adding a New Dotfile - -1. Place source file in appropriate `roles/*/files/` directory -2. Use `link_dotfile` role in playbook: -```yaml -- include_role: - name: roles/link_dotfile - vars: - link_dotfile_src: "{{ dotfiles_dir }}/roles/myapp/files/config" - link_dotfile_dst: "{{ home_dir }}/.config/myapp/config" -``` - -### Adding a New Ansible Role - -1. Create role directory: `roles/new-role/` -2. Add `tasks/main.yml` with role logic -3. Add `defaults/main.yml` for default variables (optional) -4. Include role in appropriate playbook (`local_bootstrap.yml` or `nas_bootstrap.yml`) -5. Add role tags for selective execution - -### Adding macOS Applications - -Edit `roles/macos/defaults/main.yml`: -- CLI tools → `brew_packages` -- GUI apps → `brew_cask_packages` -- App Store apps → `mas_apps` (requires app ID from App Store) diff --git a/.claude/agents/ansible-idempotency-reviewer.md b/.claude/agents/ansible-idempotency-reviewer.md new file mode 100644 index 0000000..01aab7d --- /dev/null +++ b/.claude/agents/ansible-idempotency-reviewer.md @@ -0,0 +1,74 @@ +--- +name: ansible-idempotency-reviewer +description: Use to review changes to this dotfiles repo's Ansible roles (`roles/*/tasks/*.yml`), playbooks (`playbooks/*.yml`), and custom modules (`library/*.py`) for idempotency, re-runnability, and provisioning safety. Dispatch after editing or adding role tasks, before committing provisioning changes, or when a playbook reports `changed` on a converged host or misbehaves on a re-run. It audits change detection, module vs shell usage, guard conditions, FQCN/convention conformance, and secret handling — then reports concrete, located findings.\n\n\nContext: The user just added tasks to an existing role.\nuser: "I added a task to roles/docker/tasks/main.yml to pull the compose images — does it look right?"\nassistant: "I'll dispatch the ansible-idempotency-reviewer agent to audit the new tasks for idempotency and the repo's role conventions."\n\nNew role tasks must be idempotent and follow the repo's FQCN/tag/link_dotfile conventions — exactly this agent's remit.\n\n\n\n\nContext: A play reports changed every run.\nuser: "Re-running the local bootstrap always shows the git config task as changed."\nassistant: "Let me use the ansible-idempotency-reviewer agent to find the missing change-detection guard in the git role."\n\nA task that is perpetually `changed` on a converged host is the agent's core failure mode to catch.\n\n +model: sonnet +color: yellow +--- + +You are a focused, read-leaning reviewer of Ansible provisioning content for a personal dotfiles repository. Your remit +is the `roles/` tree (each role's `tasks/`, `handlers/`, `defaults/`, `templates/`), the `playbooks/` +(`local_bootstrap.yml`, `nas_bootstrap.yml`), and custom modules under `library/`. You audit for **idempotency, +re-runnability, and provisioning safety** — the failure modes that make a converged host report `changed`, or that +brick/corrupt a real machine setup. You do not rewrite the content; you report precise, located findings so the main +agent can fix them. + +## Repository contract (ground truth — verify against the actual files, do not assume) + +- Roles are discovered via `roles_path=./roles:…` (`ansible.cfg`); custom modules via `library=./library:…`. +- Tasks use **fully-qualified collection names** (`ansible.builtin.file`, `ansible.builtin.template`, …) — the existing + roles are consistently FQCN. +- Symlinking dotfiles goes through the reusable **`link_dotfile`** role + (`ansible.builtin.include_role: roles/link_dotfile` with `link_dotfile_src`/`link_dotfile_dst`), which already handles + source validation, parent-dir creation, timestamped backup of non-symlinks, and idempotent (re)linking. New symlink + logic should reuse it, not hand-roll `file`/`command`. +- Tasks carry **tags** (e.g. `configfile`, `preferences`) for selective execution; new tasks/blocks should be tagged + consistently with their role. +- `mode:` is typically driven by a role `defaults/` variable (e.g. `git_config_mode`), not a hardcoded literal. +- Secrets are `ansible-vault`-encrypted in the separate `inventory/` clone; `vault_password_file=vault_password.txt`. +- Both playbooks run with `--ask-become-pass`; `become` is scoped per-task/block, not assumed global. + +## What to check, in priority order + +1. **Idempotency / change detection.** Every task must be a no-op on a converged host. Flag: + `ansible.builtin.command`/`shell` without `creates`/`removes`/`changed_when`/a guarding `when` (raw `command`/`shell` + defaults to always-`changed`); `lineinfile`/`blockinfile` preferred over `command: … >> file`; `register` + + `changed_when`/`failed_when` where a command's own rc is not a faithful change signal; `get_url`/`unarchive` with a + `dest`/`creates` guard. A hand-rolled `command: mv …` is acceptable only when correctly gated (e.g. `when:` + + explicit `changed_when`), as `link_dotfile` does for backups. +2. **Module over shell.** Prefer a real module to `command`/`shell` whenever one exists (`file`, `copy`, `template`, + `git`, `homebrew`, `homebrew_cask`, `apt`, `pip`, `systemd`, `lineinfile`). Flag shell-outs that reinvent a module. +3. **Convention conformance.** FQCN module names; role-appropriate `tags`; `mode` sourced from a `defaults/` var rather + than hardcoded; symlinks routed through `link_dotfile`; handlers + `notify` used for service restarts rather than an + inline restart task. +4. **Provisioning safety.** No destructive task (`file: state=absent`, `command: rm/mv`) without a guarding `when:`; no + plaintext secret that belongs in vault; `become` present where privilege is required and absent where it is not; + `check_mode`/`--check` friendliness (no un-guarded side effects in `command`). +5. **Correctness under re-run.** `register`ed facts referenced with `| default(...)` where the task may be skipped; + `when:` conditions that reference stat results guard on `.stat.exists`. +6. **README drift.** `roles/README.md` (and per-role docs) describe the role set; a newly added or renamed role should + have a corresponding entry. Flag the mismatch. + +## Method + +1. Identify the changed/target files (ask or use the provided diff; otherwise inspect `roles/`/`playbooks/`). +2. Skim `roles/link_dotfile/tasks/main.yml` and the target role's `defaults/main.yml` to confirm available variables and + the reuse pattern, then read each target `tasks` file in full. +3. For each task, trace: is it idempotent (no-op on re-run)? does change detection reflect reality? is a module used + where one exists? are FQCN/tags/mode conventions followed? Confirm suspicions by reading the relevant lines — do not + guess. +4. Cross-check `roles/README.md` when roles were added or renamed. + +## Output format + +Lead with a one-line verdict (idempotent & safe / issues found). Then list findings, most severe first: + +``` + + +- roles/docker/tasks/main.yml:24 — [idempotency] `command: docker compose pull` has no `changed_when`; reports changed every run. Add `changed_when: false` (read-ish) or gate on image digest. +- roles/git/tasks/main.yml:31 — [convention] symlink hand-rolled with `file: state=link`; reuse the `link_dotfile` role instead. +- roles/README.md — [drift] no entry for the new `tailscale` role. +``` + +Each finding: `file:line — [category] problem → concrete fix`. If a task file is clean, say so explicitly and name what +you verified (change detection, module usage, conventions, safety). Do not pad with praise or restate the whole file. diff --git a/.claude/rules/python-style.md b/.claude/rules/python-style.md new file mode 100644 index 0000000..5e3223a --- /dev/null +++ b/.claude/rules/python-style.md @@ -0,0 +1,45 @@ +--- +globs: '**/*.py' +--- + +# Python Style + +Self-contained Python style for this repository (does not depend on any global `~/.claude/rules/`). Python here is +`run.py`, custom Ansible modules under `library/`, and helper scripts under `scripts/`. + +## Tooling (non-negotiable) + +- Always run Python via **`uv`** (`uv run …`) — never bare `python`, `python3`, `pip`, or `hatch`. +- `ruff check --fix` is encouraged. **Never** run `ruff format` — this repo does not use the formatter. +- Config lives in `pyproject.toml`: ruff `line-length = 120`, `target-version = "py39"` (`keep-runtime-typing = true`), + space indent (width 4). `interrogate` enforces **70%** docstring coverage (`fail-under = 70`). + +## Imports + +- `from __future__ import annotations` is a **required first import** (isort `required-imports`). +- isort uses **`force-single-line = true`** — one import per line; never `from x import a, b`. +- Ordering: standard library → third-party → local → `if TYPE_CHECKING:` block. +- `TC` (flake8-type-checking) is on: move type-only imports into `if TYPE_CHECKING:` blocks. `pydantic.BaseModel` + subclasses are runtime-evaluated (exempt). + +## Types & naming + +- Python 3.9 syntax: `list[str]`, `dict[str, Any]`, `str | None` (via `from __future__ import annotations`). +- Files/modules snake_case; classes PascalCase; functions/methods snake_case; constants UPPER_SNAKE_CASE. +- No bare `Any` without justification (`ANN401` is otherwise ignored repo-wide, so use it sparingly and deliberately). +- No mutable default arguments. Exception chaining required: `raise NewError(...) from e` (or `from None`). +- Public **modules** define `__all__` after imports; scripts executed directly (e.g. `run.py`) do not. + +## Quotes & docstrings + +- **Single quotes** for inline strings (`flake8-quotes` inline-quotes = single). +- Docstring convention is **sphinx** (`interrogate style = "sphinx"`): colon-terminated section headers (`Examples:`, + `See Also:`) and Sphinx field tags (`:param:`, `:raises:`, `:return:`). There is **no** enforced summary-line period + and **no** dashed section underlines (that is the numpy convention, which this repo does not use). +- No blank line between a class docstring and its first member. + +## `run.py` patterns + +Commands register via the `@command` decorator into a global registry that drives both argparse and Makefile generation; +subprocess calls go through the typed `shell_command()` wrapper. Match these when adding commands. See +[`.ctx/CONVENTIONS.md`](../../.ctx/CONVENTIONS.md). diff --git a/.claude/rules/readme-guidelines.md b/.claude/rules/readme-guidelines.md new file mode 100644 index 0000000..39d259f --- /dev/null +++ b/.claude/rules/readme-guidelines.md @@ -0,0 +1,34 @@ +--- +globs: '**/README.md' +--- + +# README Guidelines + +Self-contained conventions for per-directory `README.md` files (does not depend on any global `~/.claude/rules/`). + +## Core principles + +- **Factual accuracy — never guess.** Document only what the source proves (signatures, `__all__`, docstrings, task + names, call sites). Anything the code can't answer becomes an explicit "Clarifications needed" question, never a + plausible-sounding guess. +- **Signal over noise.** Every line must help a reader understand what a module does or how it is structured. Cut filler + ("this file is intentionally empty", restating a filename). +- **Diagrams only when they clarify** a non-trivial architecture or flow — never by default. +- **Relative links only.** Never hard-code absolute home-directory paths. + +## Navigation contract + +- Up-breadcrumb at the very top (repo root has none): + `[Project Root](../README.md) > [Parent](../README.md) > **This Module**` +- Down-links: a section listing each immediate child directory that has a README. +- Every source/module directory carries its own `README.md` — `roles/`, `services/`, `playbooks/`, `scripts/install/`, + and `docs/` already follow this. + +## Repo specifics + +- **Markdown is auto-formatted by `mdformat` via the `prek` hook** (`.pre-commit-config.yaml`, priority 30) at the + **120-char** line width. `.ctx/` and `docs/plans/` are excluded, so those stay hand-maintained. Run + `prek run mdformat --all-files` (or let the commit hook do it) rather than formatting by hand. +- Files with 100+ lines and 3+ h2 sections should carry the mdformat-toc anchor block (`` + / ``) immediately after the H1 description; the hook populates it. Shorter files omit it. +- Never run `ruff format` on markdown. diff --git a/.claude/rules/script-style.md b/.claude/rules/script-style.md new file mode 100644 index 0000000..f1ed9d2 --- /dev/null +++ b/.claude/rules/script-style.md @@ -0,0 +1,30 @@ +--- +globs: '**/*.py' +--- + +# Script Styling & Console Output + +Self-contained conventions for styled terminal output in CLI scripts (does not depend on any global `~/.claude/rules/`). + +## Reference implementation + +[`run.py`](../../run.py) is the canonical in-repo implementation of the styling contract — match its patterns when +adding styled output to `run.py` or scripts under [`scripts/`](../../scripts) (`env/`, `install/`). It provides: + +| Function | Purpose | +| ------------ | ------------------------------------------------------------------------- | +| `style()` | Apply ANSI escape codes to text; returns a styled string (does not print) | +| `printf()` | Print styled text with indent / debug / verbose support | +| `folduser()` | Replace `$HOME` with `~` in path strings for display | +| `unstyle()` | Strip all ANSI escape sequences from a string | + +It also uses a `RawTextHelpFormatter`-based argparse setup with a typed `Args(argparse.Namespace)` subclass; follow that +shape for new CLI options. + +## Conventions + +- Route verbose/debug output to **stderr** so it never contaminates stdout. +- Use emoji-prefixed status lines (🟢 available, 🔴 failed, 🟡 warning, ✅ success) with a reinforcing text color; action + messages (🔎 analyzing, 🧹 cleaning, 📦 building) use a cyan-ish accent. Keep it consistent with `run.py`. +- Custom **Ansible modules** live in [`library/`](../../library) (e.g. `configure_network_interfaces.py`, + `osx_pmset.py`); they follow the same Python style rules but are executed by Ansible, not via `uv`. diff --git a/.claude/settings.json b/.claude/settings.json index 3975e47..a6577d7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,4 +1,33 @@ { + "defaultMode": "acceptEdits", + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "if [[ \"$CLAUDE_TOOL_ARG_FILE_PATH\" == *.py ]]; then uv run ruff check --fix-only \"$CLAUDE_TOOL_ARG_FILE_PATH\" 2>/dev/null; fi; exit 0" + }, + { + "type": "command", + "command": "f=\"$CLAUDE_TOOL_ARG_FILE_PATH\"; if [[ \"$f\" == *.yml || \"$f\" == *.yaml ]]; then uv run yamllint \"$f\" 2>&1 || true; fi; exit 0" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "if echo \"$CLAUDE_TOOL_ARG_FILE_PATH\" | grep -qE '(vault_password\\.txt|/vault_password|inventory/)'; then echo 'BLOCKED: Cannot edit vault password or inventory files — these are sensitive and managed separately' >&2; exit 2; fi" + } + ] + } + ] + }, "permissions": { "allow": [ "Bash(ansible-lint:*)", @@ -7,6 +36,7 @@ "Bash(brew info:*)", "Bash(cat:*)", "Bash(cmp:*)", + "Bash(cp:*)", "Bash(echo:*)", "Bash(find:*)", "Bash(gh pr diff:*)", @@ -23,11 +53,13 @@ "Bash(hatch env:*)", "Bash(hatch run:*)", "Bash(hatch version:*)", + "Bash(head:*)", "Bash(interrogate:*)", "Bash(jq:*)", "Bash(ls:*)", "Bash(make:*)", "Bash(mkdir:*)", + "Bash(mv:*)", "Bash(mypy:*)", "Bash(nox:*)", "Bash(pip3 install:*)", @@ -38,7 +70,9 @@ "Bash(ruff:*)", "Bash(sed:*)", "Bash(shellcheck:*)", + "Bash(tail:*)", "Bash(test:*)", + "Bash(touch:*)", "Bash(tree:*)", "Bash(uv sync:*)", "Bash(uv run:*)", diff --git a/.claude/skills/ruff-autofix/SKILL.md b/.claude/skills/ruff-autofix/SKILL.md new file mode 100644 index 0000000..1d161d1 --- /dev/null +++ b/.claude/skills/ruff-autofix/SKILL.md @@ -0,0 +1,33 @@ +--- +name: ruff-autofix +description: Auto-fix ruff lint violations in recently edited Python files. Use after editing Python files to catch and fix lint issues immediately rather than waiting for commit-time pre-commit hooks. +user-invocable: false +--- + +# Ruff Auto-Fix + +After editing any Python file (`.py`), run ruff to auto-fix lint violations in-place. + +## When to Activate + +Trigger automatically after any Edit or Write to a `.py` file in the current project that is not included in +`.gitignore` rules or excluded by ruff per-file ignores (`tool.ruff.lint.per-file-ignores` in pyproject.toml or +ruff.toml). + +## Procedure + +1. Run `uv run ruff check --fix --quiet ` on the edited file +2. If ruff reports remaining unfixable violations, review them and fix manually +3. Do NOT run `ruff format` — this codebase does not use ruff's formatter + +## Rules + +- Only operate on `.py` files +- Don't run on `.py` files that are not tracked by git (based on `.gitignore` rules) +- Don't run on `.py` files that are excluded in ruff per-file ignores (`tool.ruff.lint.per-file-ignores` in + pyproject.toml or ruff.toml) +- Always use `uv run` to invoke ruff (never bare `ruff`) +- Use `--fix` to apply safe fixes automatically +- Use `--quiet` to suppress noise — only show actual errors +- If a fix changes the file in a way that breaks the edit's intent, revert the fix and address the lint issue manually +- Do not fix files outside the current change scope diff --git a/.ctx/ARCHITECTURE.md b/.ctx/ARCHITECTURE.md new file mode 100644 index 0000000..57044c8 --- /dev/null +++ b/.ctx/ARCHITECTURE.md @@ -0,0 +1,158 @@ +# Architecture + +Directory layout, key components, and provisioning flow for the dotfiles-personal repository. Loaded on demand from +[`CLAUDE.md`](../CLAUDE.md). + +## Dual Bootstrap System + +Two separate bootstrap workflows targeting different environments: + +- **`local_bootstrap.sh`** → `playbooks/local_bootstrap.yml`: Personal Mac configuration +- **`nas_bootstrap.sh`** → `playbooks/nas_bootstrap.yml`: Home NAS/server configuration + +Both scripts: + +1. Validate repository structure +2. Install Homebrew with checksum verification +3. Install Ansible via Homebrew +4. Execute corresponding Ansible playbook + +## Inventory Management + +- **Primary inventory**: `inventory/` (standalone git clone, managed by `run.py`) +- **Repository**: Cloned from `git@github.com:connormason/dotfiles-inventory.git` +- **Contains**: Host definitions, encrypted vault files for secrets +- **Setup command**: `python3 run.py update-inventory` (clones if missing, pulls if exists) +- **Recovery**: `python3 run.py update-inventory --force` (removes and re-clones if corrupted) +- **Vault password**: Stored in `vault_password.txt` (gitignored, must be created manually) + +**Note**: The inventory is NOT a git submodule. It is a separate git repository cloned into the `inventory/` directory +by `run.py`. This simplifies the mental model for personal dotfiles while maintaining version control. The directory is +excluded from the main repository via `.gitignore`. + +Inventory structure: + +``` +inventory/ +├── inventory.yml # Host definitions +├── group_vars/ +│ ├── all/vault.yml # Shared secrets +│ └── localhost/ +│ ├── vars.yml # Mac-specific variables +│ └── vault.yml # Mac-specific secrets +└── host_vars/ + └── nas/ + ├── vars.yml # NAS-specific variables + └── vault.yml # NAS-specific secrets +``` + +## Role System + +Ansible roles organized by target system: + +**macOS roles**: + +- `macos`: Homebrew packages, cask apps, Mac App Store apps +- `macos_settings`: System preferences (Finder, Dock, Activity Monitor, etc.) +- `macos_dock`: Dock configuration +- `hammerspoon`: Window management automation +- `iterm`: iTerm2 configuration +- `python`: Python tooling (pipx, uv, hatch) +- `starship`: Shell prompt configuration + +**Linux roles**: + +- `debian`: System packages and configuration +- `docker`: Docker setup with compose stack (Plex, Sonarr, Radarr, PiHole, Home Assistant, etc.) +- `zfs`: ZFS filesystem configuration +- `samba`: File sharing setup + +**Shared roles**: + +- `git`: Git configuration and gh CLI +- `ssh`: SSH client configuration +- `zsh`: Shell configuration +- `tailscale`: Tailscale VPN setup +- `link_dotfile`: Reusable role for symlinking dotfiles + +## link_dotfile Role Pattern + +Reusable role for safely creating dotfile symlinks: + +- Validates source exists +- Creates parent directories +- Backs up existing non-symlink files (with timestamp) +- Only updates if symlink missing or pointing to wrong target + +Usage details and the include-role snippet live in [`CONVENTIONS.md`](CONVENTIONS.md) "Adding a New Dotfile". + +## Python Management Script (`run.py`) + +`run.py` provides a CLI interface to repository operations. + +**Command categories**: + +- **Inventory**: `list-hosts`, `update-inventory` +- **Tool Installation**: `install-uv`, `install-hatch` +- **Codebase**: `clean`, `pre`, `makefile` + +**Key features**: + +- Type-annotated with full typing support +- ANSI styling via a custom `style()` function +- Command registration via the `@command` decorator +- Auto-generates the `Makefile` from registered commands +- Retry logic with exponential backoff for network operations + +**Environment variables**: + +- `DOTFILES_RUN_DEBUG`: Enable debug output +- `DOTFILES_INVENTORY_REPO_URL`: Override inventory repo URL + +## Docker Stack (NAS) + +Media server and home automation stack in `roles/docker/files/docker-compose.yml`, with per-service definitions under +`services/`: + +**Services**: + +- **Media**: Plex, Jellyfin, Radarr (movies), Sonarr (TV), Transmission (torrents), Prowlarr (indexer), Flaresolverr +- **Network**: PiHole (DNS/ad-blocking), Caddy (reverse proxy) +- **Automation**: Home Assistant, Glance dashboard +- **Support**: Autoplex (automated file organization) + +**Storage paths**: `/storage/media/*` mounted into containers + +## macOS Package Management Layers + +1. **Homebrew formulae** (`brew_packages` in `roles/macos/defaults/main.yml`): CLI tools +2. **Homebrew casks** (`brew_cask_packages`): GUI applications +3. **Mac App Store** (`mas_apps`): App Store apps via the `mas` CLI + +## macOS System Settings + +The `macos_settings` role configures system preferences via `defaults write` commands, applied via separate task files +in `roles/macos_settings/tasks/`: + +- Finder behavior and appearance +- Dock size/position/behavior +- Activity Monitor preferences +- Messages app settings +- Power management +- I/O devices (keyboard, trackpad) + +## Key Configuration Files + +- **`.pre-commit-config.yaml`**: Code quality hooks +- **`.yamllint.yaml`**: YAML linting rules +- **`.ansible-lint.yaml`**: Ansible best practices +- **`ansible.cfg`**: Ansible defaults (`roles_path`, `library`, `vault_password_file`) +- **`vault_password.txt`**: Ansible Vault password (gitignored, create manually) +- **`roles/requirements.yml`**: External Ansible role dependencies + +## Bootstrap Script Security + +- Homebrew installer checksum verified before execution (`local_bootstrap.sh`) +- Expected checksum: `b2ffbf7e7f451c6db3b5d1976fc6a9c2faecf58ee5e1dbf6e498643c91f0d3bc` +- Update the checksum when the Homebrew installer changes: + `curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | shasum -a 256` diff --git a/.ctx/BUILD.md b/.ctx/BUILD.md new file mode 100644 index 0000000..dbf627a --- /dev/null +++ b/.ctx/BUILD.md @@ -0,0 +1,82 @@ +# Build / Test / Run + +Full command reference for the dotfiles-personal repository. Loaded on demand from [`CLAUDE.md`](../CLAUDE.md). + +## Running Ansible Playbooks + +```bash +# Bootstrap local Mac (interactive, asks for sudo password) +chmod u+x local_bootstrap.sh && ./local_bootstrap.sh + +# Bootstrap NAS server +chmod u+x nas_bootstrap.sh && ./nas_bootstrap.sh + +# Run specific tags only +ansible-playbook playbooks/local_bootstrap.yml -i inventory/inventory.yml --ask-become-pass -vv --tags git,zsh + +# Run with extra variables +ansible-playbook playbooks/local_bootstrap.yml -i inventory/inventory.yml --ask-become-pass -e "some_var=value" +``` + +## Python Script Commands (`run.py`) + +```bash +# List available inventory hosts +python3 run.py list-hosts + +# Update inventory from remote repository +python3 run.py update-inventory + +# Clean build artifacts and caches +python3 run.py clean + +# Run prek (pre-commit) hooks on all files +python3 run.py pre + +# Install Python tooling +python3 run.py install-uv +python3 run.py install-hatch + +# Generate Makefile from run.py commands +python3 run.py makefile +``` + +## Using Make (auto-generated) + +```bash +# Show available targets +make help + +# All python3 run.py commands are available as make targets +make update-inventory +make clean +make pre +``` + +## Git Hooks (prek) + +Hooks run via [prek](https://github.com/j178/prek) (a faster pre-commit reimplementation), configured in +`.pre-commit-config.yaml`. Hooks are ordered by `priority`: dependency syncing (0–6) → read-only checks (10) → +whitespace fixers (20s) → formatters (30) → linters (40). + +- **Dependency sync**: `uv-lock`, `uv-sync`, `uv-export` (requirements.txt), `sync-with-uv`, `sync-pre-commit-deps` +- **Checks**: filesystem safety, large files, merge conflicts, private keys, `detect-secrets`, submodule ban, + shebang/executable, TOML/XML/YAML/JSON syntax, `validate-pyproject`, Python AST/debug/test-naming +- **Formatters**: JSON (`.claude`), shell (`shfmt`), TOML (`taplo`), markdown (`mdformat`), Python (`ruff --fix-only`) +- **Linters**: `ruff check`, `mypy`, `interrogate`, `shellcheck`, `yamllint`, `codespell` + +```bash +# All files +prek run --all-files + +# Or via the run.py wrapper +python3 run.py pre + +# Install / update the git hook shims +python3 run.py install-hooks +``` + +## Ansible Linting + +`ansible-lint` (with `.ansible-lint.yaml`) is available but commented out in `.pre-commit-config.yaml`; enable when +ready. `shellcheck` is already enabled as a prek linter (priority 40). diff --git a/.ctx/CONVENTIONS.md b/.ctx/CONVENTIONS.md new file mode 100644 index 0000000..c77853a --- /dev/null +++ b/.ctx/CONVENTIONS.md @@ -0,0 +1,46 @@ +# Conventions & Common Patterns + +Code style, file organization, and the recurring "how do I add X" patterns for the dotfiles-personal repository. Loaded +on demand from [`CLAUDE.md`](../CLAUDE.md). + +## Code Style + +Self-contained style rules live in [`.claude/rules/`](../.claude/rules) (they do not depend on any global +`~/.claude/rules/`): + +- [`python-style.md`](../.claude/rules/python-style.md) — ruff line-length 120, target py39, single quotes, + `force-single-line` imports, **sphinx** docstring convention, interrogate 70%. Always run Python via `uv`; never run + `ruff format`. +- [`script-style.md`](../.claude/rules/script-style.md) — `run.py` is the reference implementation of the styling + contract. +- [`readme-guidelines.md`](../.claude/rules/readme-guidelines.md) — per-directory READMEs with breadcrumbs; + auto-formatted with mdformat via prek (`.ctx/` and `docs/plans/` excluded). + +## Adding a New Dotfile + +1. Place the source file in the appropriate `roles/*/files/` directory. +2. Use the `link_dotfile` role in the playbook: + +```yaml +- include_role: + name: roles/link_dotfile + vars: + link_dotfile_src: "{{ dotfiles_dir }}/roles/myapp/files/config" + link_dotfile_dst: "{{ home_dir }}/.config/myapp/config" +``` + +## Adding a New Ansible Role + +1. Create the role directory: `roles/new-role/` +2. Add `tasks/main.yml` with the role logic (FQCN modules, tags, idempotent tasks). +3. Add `defaults/main.yml` for default variables (optional). +4. Include the role in the appropriate playbook (`local_bootstrap.yml` or `nas_bootstrap.yml`). +5. Add role tags for selective execution. + +## Adding macOS Applications + +Edit `roles/macos/defaults/main.yml`: + +- CLI tools → `brew_packages` +- GUI apps → `brew_cask_packages` +- App Store apps → `mas_apps` (requires the app ID from the App Store) diff --git a/.editorconfig b/.editorconfig index 9993f83..3234b6f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,17 +7,26 @@ end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = true -trim_trailing_whitespace = true max_line_length = 120 +trim_trailing_whitespace = true -[*.{yml,yaml}] +[*.{layout,theme}] indent_size = 2 [*.md] trim_trailing_whitespace = false +[*.pkl] +indent_size = 2 + +[*.{yml,yaml}] +indent_size = 2 + [Makefile] indent_style = tab [.claude/settings*.json] indent_size = 2 + +[*todo.md] +indent_size = 2 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ef95fc1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @connormason diff --git a/.gitignore b/.gitignore index e28636b..b4e7eeb 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,6 @@ *_LOCAL_*.txt *_REMOTE_*.txt -# Allow docker compose base file (*.BASE.* pattern above is for git merge artifacts) -!services/compose.base.yml - ### macOS ### # General .DS_Store @@ -390,7 +387,19 @@ tags # End of https://www.toptal.com/developers/gitignore/api/macos,python,git,ansible,pycharm+all,vim,VisualStudioCode,sublimetext +!services/compose.base.yml +!scripts/env/ + +.reports/ + +# Inventory private repo +/inventory/* +!/inventory/.gitkeep + +# Ansible Vault password vault_password.txt -inventory/ -!scripts/env/ +# Temp cache data +.ansible/.lock +.tmp/ +.claude/settings.local.json diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..d1526f5 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,21 @@ +# Gitleaks configuration for this repository. +# +# Extends the built-in default ruleset (see +# https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml) and layers on a +# repo-specific allowlist. Gitleaks and the gitleaks-action pick this file up automatically +# when it lives at the repository root. + +title = "dotfiles gitleaks config" + +[extend] +useDefault = true + +[allowlist] +description = "Repo-specific allowlist" +# The detect-secrets baseline (managed by the `detect-secrets` prek hook) records only SHA1 +# hashes of previously-audited findings, never plaintext secrets. Gitleaks' generic-api-key / +# high-entropy rules flag those hashes as false positives, so exclude the file from scanning — +# matching the `should_exclude_file` filter detect-secrets itself applies to this path. +paths = [ + '''\.secrets\.baseline$''', +] diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..c79fe22 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,415 @@ +# Example markdownlint configuration with all properties set to their default value + +# Default state for all rules +default: true + +# Path to configuration file to extend +extends: null + +# MD001/heading-increment +# Heading levels should only increment by one level at a time +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md001.md +MD001: + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD003/heading-style +# Heading style +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md003.md +MD003: + # Heading style + style: "consistent" + +# MD004/ul-style +# Unordered list style +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md004.md +MD004: + # List style + style: "consistent" + +# MD005/list-indent +# Inconsistent indentation for list items at the same level +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md005.md +MD005: true + +# MD007/ul-indent +# Unordered list indentation +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md007.md +MD007: + # Spaces for indent + indent: 2 + # Whether to indent the first level of the list + start_indented: false + # Spaces for first level indent (when start_indented is set) + start_indent: 2 + +# MD009/no-trailing-spaces +# Trailing spaces +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md009.md +MD009: + # Spaces for line break + br_spaces: 2 + # Include code blocks + code_blocks: false + # Allow spaces for empty lines in list items + list_item_empty_lines: false + # Include unnecessary breaks + strict: false + +# MD010/no-hard-tabs +# Hard tabs +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md010.md +MD010: + # Include code blocks + code_blocks: true + # Fenced code languages to ignore + ignore_code_languages: [] + # Number of spaces for each hard tab + spaces_per_tab: 1 + +# MD011/no-reversed-links +# Reversed link syntax +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md011.md +MD011: true + +# MD012/no-multiple-blanks +# Multiple consecutive blank lines +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md012.md +MD012: + # Consecutive blank lines + maximum: 1 + +# MD013/line-length +# Line length +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md +MD013: + # Number of characters + line_length: 120 + # Number of characters for headings + heading_line_length: 120 + # Number of characters for code blocks + code_block_line_length: 120 + # Include code blocks + code_blocks: true + # Include tables + tables: true + # Include headings + headings: true + # Strict length checking + strict: false + # Stern length checking + stern: false + +# MD014/commands-show-output +# Dollar signs used before commands without showing output +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md014.md +MD014: true + +# MD018/no-missing-space-atx +# No space after hash on atx style heading +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md018.md +MD018: true + +# MD019/no-multiple-space-atx +# Multiple spaces after hash on atx style heading +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md019.md +MD019: true + +# MD020/no-missing-space-closed-atx +# No space inside hashes on closed atx style heading +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md020.md +MD020: true + +# MD021/no-multiple-space-closed-atx +# Multiple spaces inside hashes on closed atx style heading +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md021.md +MD021: true + +# MD022/blanks-around-headings +# Headings should be surrounded by blank lines +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md022.md +MD022: + # Blank lines above heading + lines_above: 1 + # Blank lines below heading + lines_below: 1 + +# MD023/heading-start-left +# Headings must start at the beginning of the line +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md023.md +MD023: true + +# MD024/no-duplicate-heading +# Multiple headings with the same content +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md024.md +MD024: + # Only check sibling headings + siblings_only: false + +# MD025/single-title/single-h1 +# Multiple top-level headings in the same document +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md025.md +MD025: + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + # Heading level + level: 1 + +# MD026/no-trailing-punctuation +# Trailing punctuation in heading +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md026.md +MD026: + # Punctuation characters + punctuation: ".,;:!。,;:!" + +# MD027/no-multiple-space-blockquote +# Multiple spaces after blockquote symbol +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md027.md +MD027: + # Include list items + list_items: true + +# MD028/no-blanks-blockquote +# Blank line inside blockquote +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md028.md +MD028: true + +# MD029/ol-prefix +# Ordered list item prefix +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md029.md +MD029: + # List style + style: "one_or_ordered" + +# MD030/list-marker-space +# Spaces after list markers +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md030.md +MD030: + # Spaces for single-line unordered list items + ul_single: 1 + # Spaces for single-line ordered list items + ol_single: 1 + # Spaces for multi-line unordered list items + ul_multi: 1 + # Spaces for multi-line ordered list items + ol_multi: 1 + +# MD031/blanks-around-fences +# Fenced code blocks should be surrounded by blank lines +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md031.md +MD031: + # Include list items + list_items: true + +# MD032/blanks-around-lists +# Lists should be surrounded by blank lines +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md032.md +MD032: true + +# MD033/no-inline-html +# Inline HTML +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md033.md +MD033: false +# MD033: +# # Allowed elements +# allowed_elements: [] +# # Allowed elements in tables +# table_allowed_elements: [] + +# MD034/no-bare-urls +# Bare URL used +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md034.md +MD034: true + +# MD035/hr-style +# Horizontal rule style +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md035.md +MD035: + # Horizontal rule style + style: "consistent" + +# MD036/no-emphasis-as-heading +# Emphasis used instead of a heading +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md036.md +MD036: + # Punctuation characters + punctuation: ".,;:!?。,;:!?" + +# MD037/no-space-in-emphasis +# Spaces inside emphasis markers +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md037.md +MD037: true + +# MD038/no-space-in-code +# Spaces inside code span elements +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md038.md +MD038: true + +# MD039/no-space-in-links +# Spaces inside link text +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md039.md +MD039: true + +# MD040/fenced-code-language +# Fenced code blocks should have a language specified +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md040.md +MD040: + # List of languages + allowed_languages: [] + # Require language only + language_only: false + +# MD041/first-line-heading/first-line-h1 +# First line in a file should be a top-level heading +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md041.md +MD041: + # Allow content before first heading + allow_preamble: false + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + # Heading level + level: 1 + +# MD042/no-empty-links +# No empty links +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md042.md +MD042: true + +# MD043/required-headings +# Required heading structure +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md043.md +# MD043: +# # List of headings +# headings: [] +# # Match case of headings +# match_case: false + +# MD044/proper-names +# Proper names should have the correct capitalization +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md044.md +MD044: + # List of proper names + names: [] + # Include code blocks + code_blocks: true + # Include HTML elements + html_elements: true + +# MD045/no-alt-text +# Images should have alternate text (alt text) +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md045.md +MD045: true + +# MD046/code-block-style +# Code block style +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md046.md +MD046: + # Block style + style: "consistent" + +# MD047/single-trailing-newline +# Files should end with a single newline character +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md047.md +MD047: true + +# MD048/code-fence-style +# Code fence style +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md048.md +MD048: + # Code fence style + style: "consistent" + +# MD049/emphasis-style +# Emphasis style +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md049.md +MD049: + # Emphasis style + style: "consistent" + +# MD050/strong-style +# Strong style +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md050.md +MD050: + # Strong style + style: "consistent" + +# MD051/link-fragments +# Link fragments should be valid +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md051.md +MD051: + # Ignore case of fragments + ignore_case: false + # Pattern for ignoring additional fragments + ignored_pattern: "" + +# MD052/reference-links-images +# Reference links and images should use a label that is defined +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md052.md +MD052: + # Ignored link labels + ignored_labels: + - "x" + # Include shortcut syntax + shortcut_syntax: false + +# MD053/link-image-reference-definitions +# Link and image reference definitions should be needed +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md053.md +MD053: + # Ignored definitions + ignored_definitions: + - "//" + +# MD054/link-image-style +# Link and image style +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md054.md +MD054: + # Allow autolinks + autolink: true + # Allow inline links and images + inline: true + # Allow full reference links and images + full: true + # Allow collapsed reference links and images + collapsed: true + # Allow shortcut reference links and images + shortcut: true + # Allow URLs as inline links + url_inline: true + +# MD055/table-pipe-style +# Table pipe style +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md055.md +MD055: + # Table pipe style + style: "consistent" + +# MD056/table-column-count +# Table column count +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md056.md +MD056: true + +# MD058/blanks-around-tables +# Tables should be surrounded by blank lines +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md058.md +MD058: true + +# MD059/descriptive-link-text +# Link text should be descriptive +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md059.md +MD059: + # Prohibited link texts + prohibited_texts: + - "click here" + - "here" + - "link" + - "more" + +# MD060/table-column-style +# Table column style +# https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md060.md +MD060: + # Table column style + style: "any" + # Aligned delimiter columns + aligned_delimiter: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4a7cec..b56c309 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,182 +1,498 @@ --- +# prek/pre-commit configuration +# https://prek.j178.dev/configuration/ default_install_hook_types: - pre-commit - post-checkout - post-merge - post-rewrite +exclude: ^archive/ repos: + # ======================================================================================== + # Dependency syncing + # + # - Priority 0 pyproject.toml -> uv.lock + # - Priority 5 uv.lock -> .venv/ + # uv.lock -> requirements.txt + # uv.lock -> .pre-commit-config.yaml (hook versions) + # - Priority 6 .pre-commit-config.yaml -> .pre-commit-config.yaml (hook deps) + # + # ======================================================================================== - # Sync pre-commit hook dependencies based on other installed hooks - - repo: https://github.com/pre-commit/sync-pre-commit-deps - rev: v0.0.3 - hooks: - - id: sync-pre-commit-deps - name: "[sync] pre-commit hook dependencies" - - # Ensure uv.lock is up-to-date - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.11.2 + rev: 0.11.28 hooks: + + # Ensure uv.lock is up-to-date + # pyproject.toml -> uv.lock - id: uv-lock - name: "[sync] uv.lock" + name: "[sync] dependencies (pyproject.toml -> uv.lock)" + stages: [pre-commit, manual] + priority: 0 - # Synchronize dependencies with local project virtual environment on checkout/pull/rebase - - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.11.2 - hooks: + # Sync .venv/ with uv.lock on checkout/pull/rebase + # uv.lock -> .venv/ - id: uv-sync - name: "[sync] project venv" + name: "[sync] dependencies (uv.lock -> .venv)" + priority: 5 - # Checks for filesystem safety - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + # Export locked package requirements to requirements.txt + # uv.lock -> requirements.txt + - id: uv-export + name: "[sync] dependencies (uv.lock -> requirements.txt)" + stages: [pre-commit, manual] + priority: 5 + args: + - "--format=requirements.txt" + - "--output-file=requirements.txt" + - "--frozen" + - "--no-hashes" + - "--no-header" + + - repo: https://github.com/tsvikas/sync-with-uv + rev: v0.5.0 + hooks: + + # Sync pre-commit hook versions with uv.lock versions + # uv.lock -> .pre-commit-config.yaml + - id: sync-with-uv + name: "[sync] dependencies (uv.lock -> .pre-commit-config.yaml)" + stages: [pre-commit, manual] + priority: 5 + + - repo: https://github.com/pre-commit/sync-pre-commit-deps + rev: v0.0.4 + hooks: + + # Sync pre-commit hook dependencies based on other installed hooks + # .pre-commit-config.yaml -> .pre-commit-config.yaml + - id: sync-pre-commit-deps + name: "[sync] dependencies (.pre-commit-config.yaml -> .pre-commit-config.yaml)" + stages: [pre-commit, manual] + priority: 6 + + # ======================================================================================== + # Read-only checks + # + # Priority 10 (run in parallel) + # ======================================================================================== + + - repo: builtin hooks: + + # Check for files that would conflict in case-insensitive filesystems - id: check-case-conflict - name: "[check] filename case conflicts" + name: "[check] filename casing" + stages: [pre-commit, manual] + priority: 10 + + # Check for broken symlinks - id: check-symlinks - name: "[check] symlinks" + name: "[check] symlinks" + stages: [pre-commit, manual] + priority: 10 + + # Detect destroyed symlinks - id: destroyed-symlinks - name: "[check] destroyed symlinks" + name: "[check] destroyed symlinks" + stages: [pre-commit, manual] + priority: 10 - # Checks for accidentally-committed large files and files with merge conflicts - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: + # Ensures that (non-binary) files with a shebang are executable + - id: check-shebang-scripts-are-executable + name: "[check] files w/ shebangs are executable" + stages: [pre-commit, manual] + priority: 10 + exclude: \.j2$ # Jinja templates carry a shebang but are rendered, not executed + + # Ensures that (non-binary) executables have a shebang + - id: check-executables-have-shebangs + name: "[check] executable files have shebangs" + stages: [pre-commit, manual] + priority: 10 + + # Prevent committing large files - id: check-added-large-files - name: "[check] large files" + name: "[check] large files" + stages: [pre-commit, manual] + priority: 10 + exclude: | + (?x)( + ^uv\.lock| + ^.*\.md + ) + + # Check for merge conflicts (git merge strings in files) - id: check-merge-conflict - name: "[check] merge conflicts" + name: "[check] merge conflicts" + stages: [pre-commit, manual] + priority: 10 + + # Prevent addition of new submodules + - id: forbid-new-submodules + name: "[check] no new submodules" + stages: [pre-commit, manual] + priority: 10 - # Check for accidentally-committed secrets like passwords, API keys, and tokens - - repo: https://github.com/gitleaks/gitleaks - rev: v8.30.0 + # Check that VCS links are permalinks + - id: check-vcs-permalinks + name: "[check] vcs permalinks" + stages: [pre-commit, manual] + priority: 10 + types_or: [markdown, rst] + + # Detect private keys + - id: detect-private-key + name: "[check] private keys" + stages: [pre-commit, manual] + priority: 10 + + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 hooks: - - id: gitleaks - name: "[check] hardcoded secrets" - # Check that scripts with shebangs are marked executable - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + # Scan staged content for high-entropy strings / credential patterns. + # After auditing new findings, refresh the baseline: + # uvx detect-secrets scan --baseline .secrets.baseline && uvx detect-secrets audit .secrets.baseline + - id: detect-secrets + name: "[check] secrets" + stages: [pre-commit, manual] + priority: 10 + args: [--baseline, .secrets.baseline] + exclude: | + (?x)( + ^uv\.lock$| + ^\.secrets\.baseline$ + ) + + - repo: builtin hooks: - - id: check-executables-have-shebangs - name: "[check] executable shebangs" - # Check that links to VCS sites (like github.com) are permalinks - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + # Validate TOML file syntax + - id: check-toml + name: "[check] toml syntax" + stages: [pre-commit, manual] + priority: 10 + + # Validate XML file syntax + - id: check-xml + name: "[check] xml syntax" + stages: [pre-commit, manual] + priority: 10 + + # Validate YAML file syntax + - id: check-yaml + name: "[check] yaml syntax" + stages: [pre-commit, manual] + priority: 10 + exclude: ^services/homeassistant/config/ # Doesn't like !include/!secret macros + + # Validate JSON file syntax + - id: check-json + name: "[check] json syntax" + stages: [pre-commit, manual] + priority: 10 + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.25 hooks: - - id: check-vcs-permalinks - name: "[check] vcs site permalinks" - # Fixers for file whitespace and metadata + # Validate pyproject.toml + - id: validate-pyproject + name: "[check] pyproject.toml syntax/schema" + stages: [pre-commit, manual] + priority: 10 + args: ["--disable-plugins", "hatch"] + additional_dependencies: + - validate-pyproject[all] + - validate-pyproject-schema-store + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: + + # Verify python source files parse as valid python + - id: check-ast + name: "[check] python ast" + stages: [pre-commit, manual] + priority: 10 + + # Check for debugger imports and py37+ ``breakpoint()`` calls in python source files + - id: debug-statements + name: "[check] python debug statements" + stages: [pre-commit, manual] + priority: 10 + + # Verify python unit test filenames match pytest expectations + - id: name-tests-test + name: "[check] unit test filenames" + stages: [pre-commit, manual] + priority: 10 + args: [--pytest-test-first] + + # ======================================================================================== + # Text/whitespace fixers + # + # Priority 20 - 23 (run sequentially, all may modify any text file) + # ======================================================================================== + + - repo: builtin + hooks: + + # Removes UTF-8 byte order marker - id: fix-byte-order-marker - name: "[fix] utf-8 byte order marker" + name: "[fix] utf-8 byte order marker" + stages: [pre-commit] + priority: 20 + + # Ensures files end in a single newline - id: end-of-file-fixer - name: "[fix] end-of-file" + name: "[fix] end-of-file" + stages: [pre-commit] + priority: 21 + + # Replaces mixed line ending - id: mixed-line-ending - name: "[fix] mixed line ending" - exclude: .gitignore + name: "[fix] mixed line ending" + stages: [pre-commit] + priority: 22 + exclude: ^\.gitignore + + # Trims trailing whitespace from files - id: trailing-whitespace - name: "[fix] trailing whitespace" + name: "[fix] trailing whitespace" + stages: [pre-commit] + priority: 23 exclude: | (?x)( ^.gitignore| .config/istat_menus.ismp ) - # Validate pyproject.toml - - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.25 - hooks: - - id: validate-pyproject - name: "[check] pyproject.toml" + # ======================================================================================== + # Formatters/fixers + # + # - Priority 30 any file type keep-sorted + # - Priority 35 shell scripts shfmt + # json (.claude) pretty-format-json + # toml taplo + # markdown mdformat + # python ruff --fix-only + # + # ======================================================================================== - # Checks for validity of non-code filetypes - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + - repo: https://github.com/google/keep-sorted + rev: v0.9.1 hooks: - - id: check-json - name: "[check] json files" - - id: check-toml - name: "[check] toml files" - - id: check-xml - name: "[check] xml files" - - id: check-yaml - name: "[check] yaml files" - exclude: ^services/homeassistant/config/ # Doesn't like !include/!secret macros - # Fix common misspellings in text files - - repo: https://github.com/codespell-project/codespell - rev: v2.4.2 + # Language-agnostic auto-sorting between two markers + - id: keep-sorted + stages: [pre-commit] + name: "[format] sorting (keep-sorted)" + priority: 30 + + - repo: https://github.com/scop/pre-commit-shfmt + rev: v3.13.1-1 hooks: - - id: codespell - name: "[fix] common misspellings" - pass_filenames: false - args: [--write-changes] - additional_dependencies: - - tomli - # Basic checks for Python code + # Shell script auto-formatting + - id: shfmt + name: "[format] shell scripts (shfmt)" + stages: [pre-commit] + priority: 35 + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - - id: check-ast - name: "[check] python ast" - - id: debug-statements - name: "[check] python debug statements" - - id: name-tests-test - name: "[check] python unit test filenames" - args: [--pytest-test-first] - # Python code docstring coverage - - repo: https://github.com/econchick/interrogate - rev: 1.7.0 + # Format Claude Code settings JSON files + - id: pretty-format-json + name: "[format] json (.claude, indent=2)" + stages: [pre-commit] + priority: 35 + types: [text] + args: + - "--no-ensure-ascii" + - "--indent=2" + - "--autofix" + - "--top-keys=id,name,type,matcher,mode,command" + files: ^\.claude/settings.*\.json$ + + - repo: https://github.com/ComPWA/taplo-pre-commit + rev: v0.9.3 + hooks: + + # TOML file auto-formatting + - id: taplo-format + name: "[format] toml (taplo)" + stages: [pre-commit] + priority: 35 + + - repo: https://github.com/hukkin/mdformat + rev: 1.0.0 hooks: - - id: interrogate - name: "[check] python docstring coverage" - pass_filenames: false - # Python linting, formatting, import organization, typing upgrades, cleanup + # Markdown file auto-formatting + - id: mdformat + name: "[format] markdown (mdformat)" + stages: [pre-commit] + priority: 35 + exclude: | + (?x)^( + ^.ctx/.*| + ^docs/plans/.* + )$ + additional_dependencies: + - mdformat-config + - mdformat-footnote + - mdformat-front-matters + - mdformat-gfm + - mdformat-gfm-alerts + - mdformat-pyproject + - mdformat-simple-breaks + - mdformat-toc + + # Python lint auto-fixes - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.8 + rev: v0.15.20 hooks: - id: ruff - name: "[fix] python code (ruff check --fix-only)" + name: "[fix] python lint (ruff check --fix-only)" + stages: [pre-commit] + priority: 35 args: [--fix-only, --exit-non-zero-on-fix] + + # ======================================================================================== + # Linters + # + # Priority 40 (run in parallel) + # ======================================================================================== + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.20 + hooks: + + # Python lint check (must happen after --fix-only stage above) + # Check/lint and output errors as junit if running in CI - id: ruff - name: "[lint] python code (ruff check)" + name: "[lint] python lint (ruff check) - junit output" + stages: [manual] + priority: 40 + + # Python lint check (must happen after --fix-only stage above) + # Always check/lint (local or CI), output errors to stdout + - id: ruff + name: "[lint] python lint (ruff check)" + stages: [pre-commit, manual] + priority: 40 - # Python static type checking - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 + rev: v2.2.0 hooks: + + # Python static type checking - id: mypy - name: "[lint] python code (mypy)" + name: "[lint] python typing (mypy)" + stages: [pre-commit, manual] + priority: 40 additional_dependencies: + - "--index-url=https://pypi.org/simple" - types-PyYAML - types-requests - exclude: services/homeassistant/config/ + - types-toml + args: [--config-file, pyproject.toml, --junit-xml, .reports/mypy.xml] + exclude: ^services/homeassistant/config/ + + - repo: https://github.com/econchick/interrogate + rev: 1.7.0 + hooks: + + # Check python code docstring coverage + - id: interrogate + name: "[check] python docstring coverage" + stages: [pre-commit, manual] + priority: 40 + pass_filenames: false + args: [--config, pyproject.toml] + # NOTE: The exclude list is only considered when `interrogate` is run with pre-commit (prek) hooks. + # Excluded files must be duplicated ``tool.interrogate.exclude`` entry of pyproject.toml + # if `interrogate` is run outside of pre-commmit/prek. + exclude: | + (?x)( + ^\.cicd/| + ^\.claude/| + ^\.nox/| + ^\.tmp/| + ^\.worktrees/| + ^build/| + ^docs/| + ^tests/ + ) + + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 + hooks: + + # Linting for shell scripts + - id: shellcheck + name: "[lint] shell scripts" + stages: [pre-commit, manual] + priority: 40 + exclude: \.zsh$ # shellcheck does not support zsh (e.g. roles/zsh/files/p10k.zsh) + + # - repo: https://github.com/DavidAnson/markdownlint-cli2 + # rev: v0.22.0 + # hooks: + + # # Markdown linting + # - id: markdownlint-cli2 + # name: "[lint] Markdown files" + # stages: [pre-commit, manual] + # priority: 40 + + # - repo: https://github.com/ComPWA/taplo-pre-commit + # rev: v0.9.3 + # hooks: + + # # TOML linting + # - id: taplo-lint + # name: "[lint] toml" + # stages: [pre-commit, manual] + # priority: 40 + # exclude: ^archive - # YAML linting - repo: https://github.com/adrienverge/yamllint.git rev: v1.38.0 hooks: + + # YAML linting - id: yamllint - name: "[lint] yaml files" + name: "[lint] yaml (yamllint)" + stages: [pre-commit, manual] + priority: 40 + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.2 + hooks: + + # Fix common misspellings in text files + - id: codespell + name: "[check] spelling mistakes" + stages: [pre-commit, manual] + priority: 40 + pass_filenames: false + args: [--write-changes] + additional_dependencies: + - tomli # - repo: https://github.com/ansible/ansible-lint.git # rev: v25.6.1 # hooks: -# - id: ansible-lint -# name: "[lint] ansible" -# args: [--fix] -# - repo: https://github.com/koalaman/shellcheck-precommit -# rev: v0.11.0 -# hooks: -# - id: shellcheck -## args: ["--severity=warning"] # Optionally only show errors and warnings +# # Ansible linting +# - id: ansible-lint +# name: "[lint] ansible" +# stages: [pre-commit, manual] +# priority: 40 +# args: [--fix] diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..b1cce99 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,264 @@ +{ + "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_baseline_file", + "filename": ".secrets.baseline" + }, + { + "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" + }, + { + "path": "detect_secrets.filters.regex.should_exclude_file", + "pattern": [ + "uv\\.lock$", + "\\.secrets\\.baseline$" + ] + } + ], + "results": { + "ansible.cfg": [ + { + "type": "Secret Keyword", + "filename": "ansible.cfg", + "hashed_secret": "726cb38ae4b27e8b99ccdcc8bf30422eae9d8377", + "is_verified": false, + "line_number": 29 + } + ], + "docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/03-docker-security-env-vars.md": [ + { + "type": "Secret Keyword", + "filename": "docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/03-docker-security-env-vars.md", + "hashed_secret": "696bc0f488064726c1ee399095fc6ec8b469cdbf", + "is_verified": false, + "line_number": 93 + }, + { + "type": "Secret Keyword", + "filename": "docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/03-docker-security-env-vars.md", + "hashed_secret": "f7a75342741955b2b9b66b55a78e5061f321ff44", + "is_verified": false, + "line_number": 94 + } + ], + "docs/plans/2026-03/2026-03-29-tailscale-setup-design.md": [ + { + "type": "Secret Keyword", + "filename": "docs/plans/2026-03/2026-03-29-tailscale-setup-design.md", + "hashed_secret": "85b099e5d30374b2900e17ead706e219738b5485", + "is_verified": false, + "line_number": 283 + } + ], + "docs/plans/2026-03/2026-03-29-tailscale-setup-plan.md": [ + { + "type": "Secret Keyword", + "filename": "docs/plans/2026-03/2026-03-29-tailscale-setup-plan.md", + "hashed_secret": "1a40aedc73cdcc13a8ecf34325a8b32030b7fbd4", + "is_verified": false, + "line_number": 638 + }, + { + "type": "Secret Keyword", + "filename": "docs/plans/2026-03/2026-03-29-tailscale-setup-plan.md", + "hashed_secret": "85b099e5d30374b2900e17ead706e219738b5485", + "is_verified": false, + "line_number": 702 + } + ], + "local_bootstrap.sh": [ + { + "type": "Hex High Entropy String", + "filename": "local_bootstrap.sh", + "hashed_secret": "472fd429d5350124719ffb31e185be416cea1e9a", + "is_verified": false, + "line_number": 39 + } + ], + "nas_bootstrap.sh": [ + { + "type": "Hex High Entropy String", + "filename": "nas_bootstrap.sh", + "hashed_secret": "472fd429d5350124719ffb31e185be416cea1e9a", + "is_verified": false, + "line_number": 39 + } + ], + "playbooks/README.md": [ + { + "type": "Hex High Entropy String", + "filename": "playbooks/README.md", + "hashed_secret": "472fd429d5350124719ffb31e185be416cea1e9a", + "is_verified": false, + "line_number": 257 + } + ], + "services/homeassistant/config/scripts/get_lutron_cert.py": [ + { + "type": "Hex High Entropy String", + "filename": "services/homeassistant/config/scripts/get_lutron_cert.py", + "hashed_secret": "e7b4922d2dfd49a25e906bc9ce2326a6151a933e", + "is_verified": false, + "line_number": 46 + }, + { + "type": "Secret Keyword", + "filename": "services/homeassistant/config/scripts/get_lutron_cert.py", + "hashed_secret": "e7b4922d2dfd49a25e906bc9ce2326a6151a933e", + "is_verified": false, + "line_number": 46 + } + ], + "services/homeassistant/config/secrets_template.yaml": [ + { + "type": "Secret Keyword", + "filename": "services/homeassistant/config/secrets_template.yaml", + "hashed_secret": "6eef6648406c333a4035cd5e60d0bf2ecf2606d7", + "is_verified": false, + "line_number": 5 + } + ], + "services/homeassistant/config/www/hass.html": [ + { + "type": "Base64 High Entropy String", + "filename": "services/homeassistant/config/www/hass.html", + "hashed_secret": "3fff009038cc1bf1db64d7b5ced22e41da6e0383", + "is_verified": false, + "line_number": 1429 + }, + { + "type": "Base64 High Entropy String", + "filename": "services/homeassistant/config/www/hass.html", + "hashed_secret": "9df1e507a11fe7fec188f6ae41a94d5a585efd45", + "is_verified": false, + "line_number": 1430 + }, + { + "type": "Base64 High Entropy String", + "filename": "services/homeassistant/config/www/hass.html", + "hashed_secret": "f1314239c0be62f1a60d5ae461dfb0db0a58e5f4", + "is_verified": false, + "line_number": 1431 + } + ] + }, + "generated_at": "2026-07-08T11:16:50Z" +} diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..e76d59c --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,22 @@ +exclude = ["archive/**/*.toml"] + +[formatting] +align_comments = true +align_entries = true +allowed_blank_lines = 2 +array_trailing_comma = true +array_auto_expand = true +array_auto_collapse = false +clrf = false +column_width = 120 +compact_arrays = true +compact_entries = false +compact_inline_tables = true +indent_entries = false +indent_string = " " +indent_tables = false +inline_table_expand = true +reorder_arrays = false +reorder_keys = false +reorder_inline_tables = false +trailing_newline = true diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f5cdcd2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python-envs.workspaceSearchPaths": ["./**/.venv"] +} diff --git a/.yamllint.yaml b/.yamllint.yaml index 75719e4..741d08f 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -8,20 +8,42 @@ ignore: - vault.yaml rules: + anchors: enable braces: min-spaces-inside: 0 max-spaces-inside: 1 + brackets: enable + colons: enable + commas: enable comments: + level: warning min-spaces-from-content: 1 - comments-indentation: false + comments-indentation: + level: warning + ignore: + - .pre-commit-config.yaml + document-end: disable + document-start: + level: warning + empty-lines: enable + empty-values: disable + float-values: disable + hyphens: enable + indentation: enable + key-duplicates: enable + key-ordering: disable line-length: - max: 320 + max: 240 allow-non-breakable-words: true new-line-at-end-of-file: level: warning + new-lines: enable octal-values: forbid-implicit-octal: true forbid-explicit-octal: true + quoted-strings: disable + trailing-spaces: enable truthy: + level: warning ignore: - .github/workflows/*.yml diff --git a/CLAUDE-LESSONS.md b/CLAUDE-LESSONS.md new file mode 100644 index 0000000..9be9cde --- /dev/null +++ b/CLAUDE-LESSONS.md @@ -0,0 +1,26 @@ +# Lessons Learned + +Patterns and corrections captured during development to prevent repeated mistakes. Append new lessons here. + +## Format + +Each lesson follows this pattern: + +- **Context**: What was happening +- **Mistake**: What went wrong +- **Rule**: The rule to prevent recurrence + +--- + +## Lessons + +### Keep this repo's Claude config self-contained — don't assume global `~/.claude` exists + +- **Context**: Porting Claude Code infra from the work `~/dotfiles` repo into this personal repo. The work repo's + `.claude/rules/*.md` are thin "deltas" that inherit a global baseline at `~/.claude/rules/`. +- **Mistake**: Wrote the personal repo's rule files the same way — as deltas that reference `~/.claude/rules/`. But this + repo cannot guarantee the global `~/.claude` configuration is present (it is only reliably set up alongside the work + dotfiles), so the "inherited" baseline could be missing entirely, leaving the rules referencing nothing. +- **Rule**: Everything under this repo's `.claude/` (and `.ctx/`, `CLAUDE.md`) must stand on its own. Rule files carry + the full guidance rather than deferring to a global baseline; agents and skills reference only in-repo artifacts. If + the global config is later guaranteed for personal use, rules may optionally be slimmed back to deltas. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f2ad0a0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,42 @@ +# CLAUDE.md + +Personal dotfiles repository using **Ansible** for automated system configuration on macOS and Debian Linux. Manages +dotfiles, applications, system settings, and home infrastructure (a NAS with a media-server / home-automation stack). + +For detailed reference, load on-demand: + +- [`.ctx/ARCHITECTURE.md`](.ctx/ARCHITECTURE.md) — bootstrap flow, inventory, role system, `run.py`, Docker stack, macOS + layers +- [`.ctx/BUILD.md`](.ctx/BUILD.md) — full command reference (playbooks / `run.py` / make / prek / lint) +- [`.ctx/CONVENTIONS.md`](.ctx/CONVENTIONS.md) — code style pointers and the "adding a dotfile / role / macOS app" + patterns +- [`.claude/rules/`](.claude/rules) — self-contained Python / script / README style rules for this repo +- [`CLAUDE-LESSONS.md`](CLAUDE-LESSONS.md) — captured mistakes and rules to prevent recurrence; append new lessons here + +## Entry Points + +1. **`local_bootstrap.sh`** → `playbooks/local_bootstrap.yml` — personal Mac configuration. +2. **`nas_bootstrap.sh`** → `playbooks/nas_bootstrap.yml` — home NAS / server configuration. +3. **`run.py`** — repository-management CLI (inventory pull, tool installers, codebase chores). `Makefile` wraps these + (`make help`). Full command reference in [`docs/RUN_PY_REFERENCE.md`](docs/RUN_PY_REFERENCE.md). + +Both bootstrap scripts validate the repo, install Homebrew (checksum-verified) and Ansible, then run their playbook. See +[`.ctx/ARCHITECTURE.md`](.ctx/ARCHITECTURE.md) for the flow and [`.ctx/BUILD.md`](.ctx/BUILD.md) for invocation (tags / +extra-vars). + +## Inventory + +Stored in a separate git repo (`git@github.com:connormason/dotfiles-inventory.git`), cloned into `inventory/` by +`python3 run.py update-inventory` — **not** a submodule, and gitignored. Contains host definitions and vault-encrypted +secrets. See [`.ctx/ARCHITECTURE.md`](.ctx/ARCHITECTURE.md) "Inventory Management" for the layout. + +## Vault Password + +All secrets are `ansible-vault`-encrypted in the `inventory/` clone. The password lives in `vault_password.txt` +(gitignored, created manually; referenced by `ansible.cfg`'s `vault_password_file`). Never commit the vault password or +decrypted secrets. + +## Plans & Docs + +Design docs and implementation plans live under [`docs/plans//`](docs/plans/), named +`YYYY-MM-DD--{design,plan}.md`. Other extended docs are indexed in [`docs/README.md`](docs/README.md). diff --git a/Makefile b/Makefile index a6427e4..853b7ba 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # # Auto-generated via `run.py makefile` # -.PHONY: help list-hosts inventory-status update-inventory vault-decrypt vault-encrypt install-uv install-hatch clean pre makefile +.PHONY: help list-hosts inventory-status update-inventory vault-decrypt vault-encrypt install-uv install-hatch clean pre install-hooks uninstall-hooks makefile BRIGHT_GREEN := \033[0;92m BRIGHT_WHITE := \033[0;97m @@ -26,7 +26,9 @@ help: ## Show this help message @echo "" @echo "$(BRIGHT_WHITE)Codebase:$(NC)" @echo " $(YELLOW)clean $(NC) Remove all environments, build artifacts, and caches" - @echo " $(YELLOW)pre $(NC) Run pre-commit hooks on all project files" + @echo " $(YELLOW)pre $(NC) Run prek (pre-commit) hooks on all project files" + @echo " $(YELLOW)install-hooks $(NC) Install prek git hook shims (overwrites any existing shims)" + @echo " $(YELLOW)uninstall-hooks $(NC) Uninstall prek git hook shims" @echo " $(YELLOW)makefile $(NC) Generate Makefile from project management script commands (run.py)" @echo "" @@ -57,8 +59,14 @@ install-hatch: ## Install hatch Python project manager clean: ## Remove all environments, build artifacts, and caches @python3 run.py clean -pre: ## Run pre-commit hooks on all project files +pre: ## Run prek (pre-commit) hooks on all project files @python3 run.py pre +install-hooks: ## Install prek git hook shims (overwrites any existing shims) + @python3 run.py install-hooks + +uninstall-hooks: ## Uninstall prek git hook shims + @python3 run.py uninstall-hooks + makefile: ## Generate Makefile from project management script commands (run.py) @python3 run.py makefile diff --git a/README.md b/README.md index 6ce918a..6083855 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Connor's Dotfiles -Personal dotfiles repository using Ansible for automated system configuration on macOS and Debian Linux systems. -Manages dotfiles, applications, system settings, and home infrastructure (NAS with media server stack). +Personal dotfiles repository using Ansible for automated system configuration on macOS and Debian Linux systems. Manages +dotfiles, applications, system settings, and home infrastructure (NAS with media server stack). ## Quick Start @@ -38,6 +38,7 @@ This repository implements a dual-bootstrap system targeting two distinct enviro ### macOS Configuration (`local_bootstrap.yml`) Configures personal Mac with: + - **Development tools**: Git, Python tooling (uv, hatch, pipx), SSH, zsh with plugins - **GUI applications**: Homebrew casks and Mac App Store apps - **System settings**: Finder, Dock, Activity Monitor, power management @@ -47,6 +48,7 @@ Configures personal Mac with: ### NAS/Server Configuration (`nas_bootstrap.yml`) Configures home NAS server with: + - **Storage**: ZFS filesystem management - **File sharing**: Samba (SMB/CIFS) - **Media stack**: Plex, Sonarr, Radarr, Transmission, Prowlarr (via Docker) @@ -56,31 +58,34 @@ Configures home NAS server with: ### Core Components -| Component | Purpose | Documentation | -|-----------|---------|---------------| -| **run.py** | Python CLI for repository management | [📖 docs/RUN_PY_REFERENCE.md](docs/RUN_PY_REFERENCE.md) | -| **playbooks/** | Ansible playbooks and execution workflows | [📖 playbooks/README.md](playbooks/README.md) | -| **roles/** | Ansible roles for system configuration | [📖 roles/README.md](roles/README.md) | -| **inventory/** | Host definitions and encrypted vault files | Managed via `run.py update-inventory` | -| **cc-statusline/** | Custom Claude Code statusline | [📖 cc-statusline/README.md](cc-statusline/README.md) | +| Component | Purpose | Documentation | +| ------------------ | ------------------------------------------ | ------------------------------------------------------- | +| **run.py** | Python CLI for repository management | [📖 docs/RUN_PY_REFERENCE.md](docs/RUN_PY_REFERENCE.md) | +| **playbooks/** | Ansible playbooks and execution workflows | [📖 playbooks/README.md](playbooks/README.md) | +| **roles/** | Ansible roles for system configuration | [📖 roles/README.md](roles/README.md) | +| **inventory/** | Host definitions and encrypted vault files | Managed via `run.py update-inventory` | +| **cc-statusline/** | Custom Claude Code statusline | [📖 cc-statusline/README.md](cc-statusline/README.md) | ## Documentation ### User Guides - **[Python CLI Reference](docs/RUN_PY_REFERENCE.md)** - Complete `run.py` command reference + - Inventory management (list-hosts, update-inventory) - Tool installation (install-uv, install-hatch) - Codebase maintenance (clean, pre, makefile) - Architecture details and extension guide - **[Playbook Workflows](playbooks/README.md)** - Ansible playbook execution guide + - Bootstrap scripts with security features - Tag system for selective execution - Troubleshooting common issues - Best practices for development workflow - **[Ansible Roles](roles/README.md)** - Role system overview + - 17 roles organized by platform (macOS, Linux, Shared) - Role dependencies and usage patterns - Creating new roles @@ -91,18 +96,21 @@ Configures home NAS server with: #### System Configuration - **[macOS Role](roles/macos/README.md)** - Package management layers + - Homebrew formulae (CLI tools) - Homebrew casks (GUI applications) - Mac App Store apps - Adding/removing applications - **[Docker Role](roles/docker/README.md)** - NAS media stack + - 10+ service architecture (Plex, Sonarr, Radarr, etc.) - Storage layout and network configuration - Service management and troubleshooting - Adding new services - **[link_dotfile Role](roles/link_dotfile/README.md)** - Reusable dotfile linking pattern + - Safe symlink creation with backups - Usage in other roles - Best practices @@ -110,12 +118,14 @@ Configures home NAS server with: #### Developer Tools - **[cc-statusline](cc-statusline/README.md)** - Custom Claude Code statusline + - 15 modular components - 4 themes and 4 layouts - Customization guide - Development guide - **[Installer Scripts](scripts/install/README.md)** - Python tool installers + - uv and hatch installation - Retry logic and security features - Extension guide @@ -132,7 +142,8 @@ Configures home NAS server with: ### Inventory Management -The inventory repository is a separate private git repository containing host definitions and Ansible Vault encrypted secrets. It is managed as a standalone clone in the `inventory/` directory. +The inventory repository is a separate private git repository containing host definitions and Ansible Vault encrypted +secrets. It is managed as a standalone clone in the `inventory/` directory. **Initial setup:** @@ -190,7 +201,7 @@ python3 run.py update-inventory # Clean build artifacts python3 run.py clean -# Run pre-commit hooks +# Run prek hooks python3 run.py pre # Generate Makefile from run.py commands @@ -275,11 +286,16 @@ dotfiles-personal/ │ └── configure_network_interfaces.py │ ├── docs/ # Documentation -│ └── RUN_PY_REFERENCE.md # Python CLI reference +│ ├── RUN_PY_REFERENCE.md # Python CLI reference +│ ├── notes/ # Informal working notes +│ └── plans/ # Design docs & implementation plans (YYYY-MM/) │ +├── .ctx/ # On-demand Claude reference docs (architecture, build, conventions) ├── .ansible-lint.yaml # Ansible linting rules ├── .yamllint.yaml # YAML linting rules -└── .pre-commit-config.yaml # Pre-commit hooks configuration +├── .pre-commit-config.yaml # prek / pre-commit hook configuration +├── CLAUDE.md # Claude Code project instructions +└── CLAUDE-LESSONS.md # Captured rules to prevent recurring mistakes ``` ## Key Features @@ -287,10 +303,12 @@ dotfiles-personal/ ### Dual Bootstrap System Two separate bootstrap workflows targeting different environments: + - **`local_bootstrap.sh`** → `playbooks/local_bootstrap.yml`: Personal Mac configuration - **`nas_bootstrap.sh`** → `playbooks/nas_bootstrap.yml`: Home NAS/server configuration Both scripts: + 1. Validate repository structure 2. Install Homebrew with checksum verification 3. Install Ansible via Homebrew @@ -312,6 +330,7 @@ Both scripts: ### Docker Media Stack (NAS) Comprehensive media server and home automation: + - **Media services**: Plex, Sonarr, Radarr, Transmission, Prowlarr, Flaresolverr - **Network services**: PiHole (DNS/ad-blocking) - **Automation**: Home Assistant, Glance dashboard @@ -327,6 +346,7 @@ Comprehensive media server and home automation: ### Python CLI (`run.py`) Feature-rich command-line interface: + - **Command registration** via decorator pattern - **ANSI styling** for rich terminal output - **Retry logic** with exponential backoff for network operations @@ -336,6 +356,7 @@ Feature-rich command-line interface: ### Custom Claude Code Statusline Modular statusline system with: + - **15 components** (git, model, tokens, cost, etc.) - **4 themes** (default, emoji, minimal, neon) - **4 layouts** (minimal, default, full, connor) @@ -346,6 +367,7 @@ Modular statusline system with: ### Adding Applications (macOS) Edit `roles/macos/defaults/main.yml`: + - CLI tools → `brew_packages` - GUI apps → `brew_cask_packages` - App Store apps → `mas_apps` (requires app ID) @@ -371,7 +393,7 @@ See [roles/README.md](roles/README.md) for details. ### Testing Changes ```bash -# Run pre-commit hooks +# Run prek hooks python3 run.py pre # Test specific role with check mode @@ -390,28 +412,31 @@ ansible-playbook playbooks/local_bootstrap.yml \ ## Environment Variables -| Variable | Purpose | Default | -|----------|---------|---------| -| `DOTFILES_RUN_DEBUG` | Enable debug output in run.py | `false` | -| `DOTFILES_INVENTORY_REPO_URL` | Inventory repository URL | `git@github.com:connormason/dotfiles-inventory.git` | +| Variable | Purpose | Default | +| ----------------------------- | ----------------------------- | --------------------------------------------------- | +| `DOTFILES_RUN_DEBUG` | Enable debug output in run.py | `false` | +| `DOTFILES_INVENTORY_REPO_URL` | Inventory repository URL | `git@github.com:connormason/dotfiles-inventory.git` | ## Troubleshooting ### Inventory Issues **Inventory directory not found:** + ```bash # Clone inventory repository python3 run.py update-inventory ``` **Inventory out of sync or corrupted:** + ```bash # Force re-clone from remote python3 run.py update-inventory --force ``` **SSH authentication errors:** + 1. Verify SSH key is added to GitHub account 2. Test SSH connection: `ssh -T git@github.com` 3. Check SSH agent has key loaded: `ssh-add -l` @@ -429,7 +454,7 @@ See [run.py documentation](docs/RUN_PY_REFERENCE.md#inventory-management) for de 1. Verify if Homebrew installer was legitimately updated at https://github.com/Homebrew/install 2. Generate new checksum: -`curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | shasum -a 256` + `curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | shasum -a 256` 3. Update `EXPECTED_CHECKSUM` in bootstrap scripts ### Role Failures @@ -444,6 +469,7 @@ See [playbooks/README.md](playbooks/README.md#troubleshooting) for comprehensive ## Pre-commit Hooks Configured checks include: + - **File integrity**: Large files, merge conflicts, private keys, symlinks - **Python**: AST validation, debug statements, ruff linting, mypy type checking, interrogate docstring coverage - **Data formats**: JSON, YAML, TOML, XML validation @@ -451,9 +477,10 @@ Configured checks include: - **Fixers**: Whitespace, line endings, UTF-8 BOM Run hooks: + ```bash # All files -pre-commit run --all-files +prek run --all-files # Or via script python3 run.py pre diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..33347de --- /dev/null +++ b/docs/README.md @@ -0,0 +1,29 @@ +[Project Root](../README.md) > **Docs** + +# Docs + +Extended documentation for the dotfiles repository. These files cover specific topics in depth without duplicating what +lives in the root [`README.md`](../README.md), the on-demand [`.ctx/`](../.ctx/) quick-reference files, or the project +guidance in [`CLAUDE.md`](../CLAUDE.md). + +## Reference + +| File | Purpose | +| ------------------------------------------ | ------------------------------------------------------------------ | +| [RUN_PY_REFERENCE.md](RUN_PY_REFERENCE.md) | Full command reference for the `run.py` repository-management CLI. | + +## Notes + +Informal, working documents — not authoritative. + +| File | Purpose | +| ------------------------------ | --------------------------------------------- | +| [notes/todo.md](notes/todo.md) | Informal backlog of ideas and deferred tasks. | + +## Plans + +The [`plans/`](plans/) subdirectory holds design and implementation documents for larger features and refactors, grouped +into `plans//` monthly subdirectories and named `YYYY-MM-DD--{design,plan}.md` (`-design` for +design/brainstorm docs, `-plan` for implementation plans). Multi-document efforts may instead be a dated subdirectory +(e.g. [`plans/2026-01/2026-01-21-dotfiles-improvement-plan/`](plans/2026-01/2026-01-21-dotfiles-improvement-plan/)). +These are working documents and may be incomplete or superseded. diff --git a/docs/RUN_PY_REFERENCE.md b/docs/RUN_PY_REFERENCE.md index 555dc45..2d52c7a 100644 --- a/docs/RUN_PY_REFERENCE.md +++ b/docs/RUN_PY_REFERENCE.md @@ -1,7 +1,7 @@ # run.py - Command Reference -Python CLI tool for managing the dotfiles repository. Provides commands for inventory management, tool installation, -and codebase maintenance with ANSI styling and retry logic for robustness. +Python CLI tool for managing the dotfiles repository. Provides commands for inventory management, tool installation, and +codebase maintenance with ANSI styling and retry logic for robustness. ## Quick Start @@ -31,6 +31,7 @@ python3 run.py list-hosts ``` **Output example:** + ``` Available Bootstrap Targets ============================================================ @@ -48,6 +49,7 @@ Total hosts: 2 ``` **Error handling:** + - Checks if `inventory/inventory.yml` exists - Validates YAML structure - Suggests running `update-inventory` if missing @@ -61,16 +63,19 @@ python3 run.py update-inventory ``` **Behavior:** + - **If inventory exists and is git repo**: Pulls latest changes with `git pull origin main` - **If inventory exists but not git repo**: Removes directory and clones fresh - **If inventory doesn't exist**: Clones repository **Retry logic:** + - Pull operation: 3 attempts with 2s delay, exponential backoff (2x multiplier) - Clone operation: 3 attempts with 5s delay, exponential backoff (2x multiplier) - 30s timeout for pull, 60s timeout for clone **Configuration:** + - Repository URL: `DOTFILES_INVENTORY_REPO_URL` environment variable - Default: `git@github.com:connormason/dotfiles-inventory.git` @@ -97,13 +102,14 @@ python3 run.py install-uv --retries 5 --retry-delay 3 ``` **Options:** + - `-f, --force`: Force reinstall even if `uv` already installed - `-v, --verbose`: Enable verbose output for debugging - `--retries N`: Maximum number of download retry attempts - `--retry-delay SECONDS`: Initial delay in seconds between retries with exponential backoff -**Implementation:** -Delegates to `scripts/install/install_uv.py` (see [scripts/install/README.md](../scripts/install/README.md) for details). +**Implementation:** Delegates to `scripts/install/install_uv.py` (see +[scripts/install/README.md](../scripts/install/README.md) for details). #### `install-hatch` @@ -117,11 +123,10 @@ python3 run.py install-hatch python3 run.py install-hatch --force --verbose ``` -**Options:** -Same as `install-uv` (see above). +**Options:** Same as `install-uv` (see above). -**Implementation:** -Delegates to `scripts/install/install_hatch.py` (see [scripts/install/README.md](../scripts/install/README.md) for details). +**Implementation:** Delegates to `scripts/install/install_hatch.py` (see +[scripts/install/README.md](../scripts/install/README.md) for details). ### Codebase Maintenance @@ -138,15 +143,18 @@ python3 run.py clean **Cleaned patterns:** **Package build artifacts:** + - `build/` - `dist/` - `*.egg-info` **Package cache files:** + - `**/__pycache__/` - `**/*.pyc` **Tool cache files:** + - `.mypy_cache` - `.pytest_cache` - `.ruff_cache` @@ -155,6 +163,7 @@ python3 run.py clean - `.coverage` **Output:** + ``` 🧹 Cleaning project workspace... Cleaning package build artifacts... @@ -163,25 +172,25 @@ python3 run.py clean ✅ Cleanup complete ``` -**Debug mode:** -Shows each file/directory removed (enable with `--debug` flag). +**Debug mode:** Shows each file/directory removed (enable with `--debug` flag). #### `pre` -Run pre-commit hooks on all project files. +Run prek hooks on all project files. ```bash python3 run.py pre ``` -**Behavior:** -Executes `pre-commit run --all-files` with live output. Does not fail on hook failures (exit code ignored). +**Behavior:** Executes `prek run --all-files` with live output. Does not fail on hook failures (exit code ignored). + +**Hooks include:** -**Pre-commit hooks include:** -- File integrity checks (large files, merge conflicts, private keys) +- File integrity checks (large files, merge conflicts, private keys, detect-secrets) - Python validation (syntax, debug statements, ruff, mypy, interrogate) - Data format validation (JSON, YAML, TOML, XML) -- YAML linting with custom config +- Formatters (shfmt, taplo, mdformat, ruff --fix-only) +- Linters (shellcheck, yamllint, codespell) - Whitespace and line ending fixers See [`.pre-commit-config.yaml`](../.pre-commit-config.yaml) for complete hook configuration. @@ -195,6 +204,7 @@ python3 run.py makefile ``` **Behavior:** + - Reads all `@command` decorated functions from `run.py` - Generates `Makefile` with targets for each command - Excludes commands marked `script_only=True` @@ -202,6 +212,7 @@ python3 run.py makefile - Includes color-coded help text **Generated Makefile usage:** + ```bash # Show help make help @@ -213,6 +224,7 @@ make pre ``` **Auto-generated structure:** + ```makefile .PHONY: help list-hosts update-inventory install-uv ... @@ -248,6 +260,7 @@ def cmd_list_hosts(args: argparse.Namespace) -> None: ``` **Decorator parameters:** + - `name`: Command name (defaults to function name with prefixes removed) - `add_arguments`: Function to add command-specific arguments to subparser - `description`: Command description (defaults to docstring) @@ -257,8 +270,7 @@ def cmd_list_hosts(args: argparse.Namespace) -> None: - `script_only`: If True, exclude from Makefile generation - `makefile_only`: If True, exclude from script help output -**Registry:** -All registered commands stored in `REGISTERED_COMMANDS` dict mapping `name -> CommandInfo`. +**Registry:** All registered commands stored in `REGISTERED_COMMANDS` dict mapping `name -> CommandInfo`. ### ANSI Styling System @@ -282,12 +294,14 @@ style('IMPORTANT', fg='red', bold=True, underline=True) ``` **Available colors:** + - Basic: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` - Bright: `bright_black`, `bright_red`, `bright_green`, etc. - 256-color codes: `0-255` - RGB tuples: `(r, g, b)` where each component is `0-255` **Helper function:** + ```python printf('Styled output', fg='green', bold=True, indent=4, debug=True) ``` @@ -314,6 +328,7 @@ shell_command( ``` **Parameters:** + - `cmd`: List of command arguments - `text`: If True, return `str` (default); if False, return `bytes` - `encoding`: Text encoding (when `text=True`) @@ -325,12 +340,13 @@ shell_command( - `timeout`: Command timeout in seconds **Error handling:** + - Raises `ShellCommandError` (extends `CalledProcessError`) with formatted output - Timeout handling with graceful process termination - Live output via PTY for real-time display -**Global environment:** -Commands inherit environment from: +**Global environment:** Commands inherit environment from: + 1. `os.environ` (system environment) 2. `RUN_COMMAND_ENVVARS` (global script environment) 3. `env` parameter (command-specific overrides) @@ -347,12 +363,14 @@ def network_operation(): ``` **Behavior:** + - Attempt 1: Immediate execution - Attempt 2: Wait `delay` seconds (2.0s) - Attempt 3: Wait `delay * backoff` seconds (4.0s) - Attempt N: Wait `delay * (backoff ** (N-1))` seconds **Output:** + ``` └─ Attempt 1/3 failed, retrying in 2.0s... └─ Attempt 2/3 failed, retrying in 4.0s... @@ -360,6 +378,7 @@ def network_operation(): ``` Used by `update-inventory` for git operations with different parameters: + - Pull: 3 attempts, 2s initial delay, 2x backoff - Clone: 3 attempts, 5s initial delay, 2x backoff @@ -368,6 +387,7 @@ Used by `update-inventory` for git operations with different parameters: ### Constants **File paths:** + ```python SCRIPT_PATH = Path(__file__) DOTFILES_DIR = SCRIPT_PATH.parent @@ -379,6 +399,7 @@ SCRIPTS_DIR = DOTFILES_DIR / 'scripts' ``` **Environment variables:** + ```python # Debug mode toggle RUN_DEBUG_ENVVAR = 'DOTFILES_RUN_DEBUG' @@ -392,12 +413,12 @@ INVENTORY_REPO_URL = os.getenv( ) ``` -**Clean patterns:** -See `CLEAN_PATTERN_GROUPS` dict for complete list of glob patterns removed by `clean` command. +**Clean patterns:** See `CLEAN_PATTERN_GROUPS` dict for complete list of glob patterns removed by `clean` command. ### Type System **Key type aliases:** + ```python PathLike = Union[str, Path] StyleColor = Union[int, tuple[int, int, int], str] @@ -406,6 +427,7 @@ AddArgumentsFunc = Callable[[argparse.ArgumentParser], None] ``` **Command info dataclass:** + ```python @dataclass class CommandInfo: @@ -439,6 +461,7 @@ except subprocess.CalledProcessError as e: ``` **Output format:** + ``` ❌ Failed to push to remote └─ Exit code: 128 @@ -451,11 +474,11 @@ except subprocess.CalledProcessError as e: ### Exception Types -**ShellCommandError:** -Raised by `shell_command()` when subprocess exits with non-zero code and `check=True`. Extends +**ShellCommandError:** Raised by `shell_command()` when subprocess exits with non-zero code and `check=True`. Extends `subprocess.CalledProcessError` with formatted `__str__()` that includes stdout/stderr. **Standard subprocess exceptions:** + - `subprocess.CalledProcessError`: Non-zero exit code - `subprocess.TimeoutExpired`: Command exceeded timeout - `KeyboardInterrupt`: User cancelled operation (caught in `main()`) @@ -474,7 +497,7 @@ python3 run.py --debug update-inventory # Install uv with custom retry behavior python3 run.py install-uv --retries 5 --retry-delay 3 -# Clean and run pre-commit +# Clean and run prek python3 run.py clean python3 run.py pre ``` @@ -530,6 +553,7 @@ def flaky_operation(): ### Adding New Commands 1. **Define command function:** + ```python @command( group='My Group', @@ -543,6 +567,7 @@ def cmd_my_command(args: argparse.Namespace) -> None: ``` 2. **Add arguments (optional):** + ```python def add_my_args(parser: argparse.ArgumentParser) -> None: parser.add_argument('--option', help='Some option') @@ -557,6 +582,7 @@ def cmd_my_command(args: argparse.Namespace) -> None: ``` 3. **Regenerate Makefile:** + ```bash python3 run.py makefile ``` @@ -630,6 +656,7 @@ CLEAN_PATTERN_GROUPS: dict[str, list[str]] = { **Symptom:** `update-inventory` fails with SSH errors **Solution:** + ```bash # Verify SSH key access ssh -T git@github.com @@ -642,6 +669,7 @@ ssh-add ~/.ssh/id_ed25519 ``` **Alternative:** Use HTTPS URL instead of SSH + ```bash export DOTFILES_INVENTORY_REPO_URL=https://github.com/connormason/dotfiles-inventory.git python3 run.py update-inventory @@ -652,6 +680,7 @@ python3 run.py update-inventory **Symptom:** `list-hosts` fails with "Failed to parse inventory file" **Solution:** + ```bash # Validate YAML syntax python3 -c "import yaml; yaml.safe_load(open('inventory/inventory.yml'))" @@ -665,6 +694,7 @@ yamllint inventory/inventory.yml **Symptom:** `install-uv` or `install-hatch` fails **Solution:** + ```bash # Run with verbose output python3 run.py install-uv --verbose @@ -681,13 +711,14 @@ python3 run.py install-uv --retries 10 --retry-delay 5 **Symptom:** `pre` command shows hook failures **Solution:** + ```bash -# Update pre-commit hooks -pre-commit autoupdate +# Update prek hooks +prek auto-update # Clear hook cache -pre-commit clean -pre-commit install-hooks +prek cache clean +prek prepare-hooks # Run again python3 run.py pre @@ -731,6 +762,7 @@ This provides better UX than `subprocess.PIPE` for long-running commands. ``` **Suggested additions:** + - Bootstrap commands to run full playbook workflows - Ansible playbook execution wrappers with tag support - Vault password management helpers diff --git a/TODO.md b/docs/notes/todo.md similarity index 99% rename from TODO.md rename to docs/notes/todo.md index 743157d..22eccb3 100644 --- a/TODO.md +++ b/docs/notes/todo.md @@ -1,4 +1,5 @@ # TODOs + - Use Watchtower for automating Docker container updates (https://containrrr.dev/watchtower/) - Portainer? (https://www.portainer.io) - lazydocker @@ -9,7 +10,9 @@ - remove requirements.txt? ## Docker Services Modularization + Remaining manual steps (require NAS/GitHub access): + 1. Audit existing NAS .env for extra variables 2. Migrate ~/docker/shared/ to ~/docker/homeassistant/shared/ on NAS 3. Set up 5 GitHub secrets (Tailscale OAuth, SSH key, NAS host, user) diff --git a/.claude/plans/01-21-2026-dotfiles-improvement-plan/01-inventory-submodule-resolution.md b/docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/01-inventory-submodule-resolution.md similarity index 100% rename from .claude/plans/01-21-2026-dotfiles-improvement-plan/01-inventory-submodule-resolution.md rename to docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/01-inventory-submodule-resolution.md diff --git a/.claude/plans/01-21-2026-dotfiles-improvement-plan/02-unified-bootstrap-script.md b/docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/02-unified-bootstrap-script.md similarity index 100% rename from .claude/plans/01-21-2026-dotfiles-improvement-plan/02-unified-bootstrap-script.md rename to docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/02-unified-bootstrap-script.md diff --git a/.claude/plans/01-21-2026-dotfiles-improvement-plan/03-docker-security-env-vars.md b/docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/03-docker-security-env-vars.md similarity index 100% rename from .claude/plans/01-21-2026-dotfiles-improvement-plan/03-docker-security-env-vars.md rename to docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/03-docker-security-env-vars.md diff --git a/.claude/plans/01-21-2026-dotfiles-improvement-plan/04-bootstrap-security-hardening.md b/docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/04-bootstrap-security-hardening.md similarity index 100% rename from .claude/plans/01-21-2026-dotfiles-improvement-plan/04-bootstrap-security-hardening.md rename to docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/04-bootstrap-security-hardening.md diff --git a/.claude/plans/01-21-2026-dotfiles-improvement-plan/05-repo-integration-strategy.md b/docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/05-repo-integration-strategy.md similarity index 100% rename from .claude/plans/01-21-2026-dotfiles-improvement-plan/05-repo-integration-strategy.md rename to docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/05-repo-integration-strategy.md diff --git a/.claude/plans/01-21-2026-dotfiles-improvement-plan/06-precommit-code-quality.md b/docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/06-precommit-code-quality.md similarity index 100% rename from .claude/plans/01-21-2026-dotfiles-improvement-plan/06-precommit-code-quality.md rename to docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/06-precommit-code-quality.md diff --git a/.claude/plans/01-21-2026-dotfiles-improvement-plan/07-role-documentation-dependencies.md b/docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/07-role-documentation-dependencies.md similarity index 100% rename from .claude/plans/01-21-2026-dotfiles-improvement-plan/07-role-documentation-dependencies.md rename to docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/07-role-documentation-dependencies.md diff --git a/.claude/plans/01-21-2026-dotfiles-improvement-plan/README.md b/docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/README.md similarity index 100% rename from .claude/plans/01-21-2026-dotfiles-improvement-plan/README.md rename to docs/plans/2026-01/2026-01-21-dotfiles-improvement-plan/README.md diff --git a/.claude/specs/2026-03-29-nas-docker-deploy-pipeline-design.md b/docs/plans/2026-03/2026-03-29-nas-docker-deploy-pipeline-design.md similarity index 100% rename from .claude/specs/2026-03-29-nas-docker-deploy-pipeline-design.md rename to docs/plans/2026-03/2026-03-29-nas-docker-deploy-pipeline-design.md diff --git a/.claude/plans/2026-03-29-nas-docker-deploy-pipeline.md b/docs/plans/2026-03/2026-03-29-nas-docker-deploy-pipeline-plan.md similarity index 100% rename from .claude/plans/2026-03-29-nas-docker-deploy-pipeline.md rename to docs/plans/2026-03/2026-03-29-nas-docker-deploy-pipeline-plan.md diff --git a/.claude/specs/2026-03-29-tailscale-setup-design.md b/docs/plans/2026-03/2026-03-29-tailscale-setup-design.md similarity index 100% rename from .claude/specs/2026-03-29-tailscale-setup-design.md rename to docs/plans/2026-03/2026-03-29-tailscale-setup-design.md diff --git a/.claude/plans/2026-03-29-tailscale-setup.md b/docs/plans/2026-03/2026-03-29-tailscale-setup-plan.md similarity index 100% rename from .claude/plans/2026-03-29-tailscale-setup.md rename to docs/plans/2026-03/2026-03-29-tailscale-setup-plan.md diff --git a/docs/superpowers/specs/2026-05-25-jellyfin-service-design.md b/docs/plans/2026-05/2026-05-25-jellyfin-service-design.md similarity index 100% rename from docs/superpowers/specs/2026-05-25-jellyfin-service-design.md rename to docs/plans/2026-05/2026-05-25-jellyfin-service-design.md diff --git a/docs/superpowers/plans/2026-05-25-jellyfin-service.md b/docs/plans/2026-05/2026-05-25-jellyfin-service-plan.md similarity index 100% rename from docs/superpowers/plans/2026-05-25-jellyfin-service.md rename to docs/plans/2026-05/2026-05-25-jellyfin-service-plan.md diff --git a/inventory/.gitkeep b/inventory/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/library/configure_network_interfaces.md b/library/configure_network_interfaces.md index 11753f3..c3c8411 100644 --- a/library/configure_network_interfaces.md +++ b/library/configure_network_interfaces.md @@ -5,8 +5,8 @@ This module allows for configuration of macOS network interfaces by wrapping the ## Parameters - `interfaces` (list): list of dictionaries describing desired port/service configuration (see - [Interface Configuration](#interface-configuration)). Each entry corresponds to a hardware port - and its associated network service on the host machine + [Interface Configuration](#interface-configuration)). Each entry corresponds to a hardware port and its associated + network service on the host machine ### Interface Configuration @@ -29,28 +29,30 @@ Sub-options for each entry in `interfaces` parameter - `address` (str, required): IPv6 address to set for interface - `prefix_length` (str, required): IPv6 prefix length to set for interface - `router` (str, optional): IPv6 router address to set for interface -- `dns_servers` (list, optional): DNS servers to set for interface. If None, existing DNS servers will be preserved. - If empty list, existing DNS servers will be cleared out -- `search_domains` (list, optional): DNS servers to set for interface. If None, existing DNS servers will be - preserved. If empty list, existing DNS servers will be cleared out +- `dns_servers` (list, optional): DNS servers to set for interface. If None, existing DNS servers will be preserved. If + empty list, existing DNS servers will be cleared out +- `search_domains` (list, optional): DNS servers to set for interface. If None, existing DNS servers will be preserved. + If empty list, existing DNS servers will be cleared out - `hardware` (dict, optional): Hardware configuration for port associated with network interface. If not provided, - hardware configuration will be automatically configured by macOS - - `speed` (int, optional): Configure speed of network port. Note that not all ports support all speeds. - Choices: 10, 100, 1000 + hardware configuration will be automatically configured by macOS + - `speed` (int, optional): Configure speed of network port. Note that not all ports support all speeds. Choices: 10, + 100, 1000 - `duplex` (str, optional): Configure duplex setting of network port. Note that not all ports support all settings, - and not all configurations support setting this at all. Choices: half-duplex, full-duplex + and not all configurations support setting this at all. Choices: half-duplex, full-duplex - `flow_control` (bool, optional): Enable flow control for network port. Note that not all configurations support - this. Required if `duplex` value is provided + this. Required if `duplex` value is provided - `energy_efficient_ethernet` (bool, optional): Enable energy efficient ethernet for network port. Note that not all - configurations support this. Required if `duplex` value is provided + configurations support this. Required if `duplex` value is provided - `mtu` (int, optional): MTU setting of network port. If not provided, default MTU setting will be used Note that the following parameters are mutually exclusive: + - `dhcp` - `dhcp_with_manual_address` - `manual` For `ipv6` option, the following parameters are mutually exclusive: + - `off` - `automatic` - `link_local` @@ -59,6 +61,7 @@ For `ipv6` option, the following parameters are mutually exclusive: ## Returns ### Always Returned + - `changed` (bool): whether any changes were made to the network configurations - `changelog` (list): list of changes made to the network configurations - `commands_run` (list): list of `networksetup` commands run during configuration @@ -66,19 +69,21 @@ For `ipv6` option, the following parameters are mutually exclusive: - `available_network_services` (list): list of information dictionaries describing network services available on host ### Returned on Failure + - `cmd` (str): `networksetup` command that was unable to run successfully - `stdout` (str): Standard out of `networksetup` command that was unable to run successfully - `stderr` (str): Standard err of `networksetup` command that was unable to run successfully ### Returned Conditionally Based on Configuration + - `valid_mtu_range_by_port` (dict): mapping from hardware port MAC address -> supported range of MTU values (min, max) - `valid_media_by_port` (dict): mapping from hardware port MAC address -> list of dictionaries describing available - media configurations for the port (speed, duplex, flow control, energy efficient - ethernet) + media configurations for the port (speed, duplex, flow control, energy efficient ethernet) ## Examples ### Configure an interface for DHCP + ``` - hosts: myhost.mydomain.com tasks: @@ -89,6 +94,7 @@ For `ipv6` option, the following parameters are mutually exclusive: ``` ### Configure an interface for DHCP w/ manual IP address and set custom service name + ``` - hosts: myhost.mydomain.com tasks: @@ -101,6 +107,7 @@ For `ipv6` option, the following parameters are mutually exclusive: ``` ### Configure two interfaces, one for DHCP, one manually + ``` - hosts: myhost.mydomain.com tasks: diff --git a/library/configure_network_interfaces.py b/library/configure_network_interfaces.py index b5283fc..4138fac 100755 --- a/library/configure_network_interfaces.py +++ b/library/configure_network_interfaces.py @@ -431,8 +431,7 @@ def parse_getmtu(stdout: str) -> Optional[CurrentMTUConfig]: """ if m := MTU_REGEX.match(stdout): return CurrentMTUConfig(current=int(m.group('current')), active=int(m.group('active'))) - else: - return None + return None def parse_listvalidmturange(stdout: str) -> Optional[tuple[int, int]]: @@ -444,8 +443,7 @@ def parse_listvalidmturange(stdout: str) -> Optional[tuple[int, int]]: """ if m := MTU_RANGE_REGEX.match(stdout): return int(m.group('min')), int(m.group('max')) - else: - return None + return None def parse_media(media_str: str) -> Optional[HardwarePortMediaConfig]: @@ -460,16 +458,15 @@ def parse_media(media_str: str) -> Optional[HardwarePortMediaConfig]: """ if m := MEDIA_CONFIG_REGEX.match(media_str): speed_int = SPEED_REVERSE_MAPPING[m.group('speed')] - media = [item.strip() for item in m.group('media').split(',')] - duplex = 'half-duplex' if 'half-duplex' in media else 'full-duplex' + media = [item.strip() for item in m.group('media').split(',')] + duplex = 'half-duplex' if 'half-duplex' in media else 'full-duplex' return HardwarePortMediaConfig( speed=speed_int, duplex=duplex, flow_control='flow-control' in media, energy_efficient_ethernet='energy-efficient-ethernet' in media, ) - else: - return None + return None def parse_listvalidmedia(stdout: str) -> list[HardwarePortMediaConfig]: @@ -504,8 +501,7 @@ def parse_getinfo(name: str, stdout: str) -> Optional[NetworkServiceInfo]: if (info['configuration'] is None) or (info['configuration'] not in INTERFACE_CONFIGURATION_MAP): return None - else: - info['configuration'] = INTERFACE_CONFIGURATION_MAP[info['configuration']] + info['configuration'] = INTERFACE_CONFIGURATION_MAP[info['configuration']] ipv6_prefix_length: int | None = None if ipv6_prefix_length_str := info.get('ipv6_prefix_length'): @@ -740,7 +736,7 @@ def networksetup_cmd(self, cmd: str, *, check_rc: bool = True, **kwargs: Any) -> :return: (return code, stdout, stderr) """ if cmd.startswith('networksetup'): - cmd = cmd.lstrip('networksetup').lstrip() + cmd = cmd.removeprefix('networksetup').lstrip() full_cmd = f'sudo {self.bin} {cmd}' self.result['commands_run'].append(full_cmd) @@ -749,8 +745,7 @@ def networksetup_cmd(self, cmd: str, *, check_rc: bool = True, **kwargs: Any) -> if (rc != 0) and check_rc: self._update_result(cmd=full_cmd, returncode=rc, stdout=stdout, stderr=stderr) raise ConfigurationError(f'Command `networksetup {cmd}` failed: {stderr}') - else: - return rc, stdout, stderr + return rc, stdout, stderr """ ``networksetup`` command execution/parsing helpers """ @@ -785,13 +780,14 @@ def get_valid_mtu_range(self, hardware_port: str) -> tuple[int, int]: """ cmd = f'networksetup -listvalidMTUrange "{hardware_port}"' rc, stdout, stderr = self.networksetup_cmd(cmd) + result = parse_listvalidmturange(stdout) if result is not None: self.result.setdefault('valid_mtu_range_by_port', {})[hardware_port] = (result[0], result[1]) return result[0], result[1] - else: - self._update_result(cmd=cmd, returncode=rc, stdout=stdout, stderr=stderr) - raise ConfigurationError(f'Unable to parse valid MTU range for hardware port "{hardware_port}"') + + self._update_result(cmd=cmd, returncode=rc, stdout=stdout, stderr=stderr) + raise ConfigurationError(f'Unable to parse valid MTU range for hardware port "{hardware_port}"') def get_port_mtu(self, hardware_port: str) -> CurrentMTUConfig: """ @@ -803,12 +799,13 @@ def get_port_mtu(self, hardware_port: str) -> CurrentMTUConfig: """ cmd = f'networksetup -getMTU "{hardware_port}"' rc, stdout, stderr = self.networksetup_cmd(cmd) + result = parse_getmtu(stdout) if result is not None: return result - else: - self._update_result(cmd=cmd, returncode=rc, stdout=stdout, stderr=stderr) - raise ConfigurationError(f'Unable to parse current/active MTU values for hardware port "{hardware_port}"') + + self._update_result(cmd=cmd, returncode=rc, stdout=stdout, stderr=stderr) + raise ConfigurationError(f'Unable to parse current/active MTU values for hardware port "{hardware_port}"') def get_port_media_configuration(self, hardware_port: str) -> CurrentHardwarePortMediaConfig: """ @@ -830,14 +827,13 @@ def get_port_media_configuration(self, hardware_port: str) -> CurrentHardwarePor current: Union[HardwarePortMediaConfig, str] if current_str == 'autoselect': current = 'autoselect' + elif media_config := parse_media(current_str): + current = media_config else: - if media_config := parse_media(current_str): - current = media_config - else: - raise ConfigurationError(f'Unable to parse current media configuration for port "{hardware_port}"') + raise ConfigurationError(f'Unable to parse current media configuration for port "{hardware_port}"') active_str = lines[1].replace('Active: ', '').strip() - active = parse_media(active_str) + active = parse_media(active_str) if active is None: raise ConfigurationError(f'Unable to parse active media configuration for port "{hardware_port}"') @@ -872,8 +868,7 @@ def get_network_service_info(self, service: str, *, update_result_on_error: bool if update_result_on_error: self._update_result(cmd=cmd, returncode=rc, stdout=stdout, stderr=stderr) raise ConfigurationError(f'Unable to parse network configuration for service "{service}"') - else: - return info + return info def get_network_services_by_mac_address(self) -> dict[str, NetworkServiceInfo]: """ @@ -899,7 +894,7 @@ def get_network_services_by_mac_address(self) -> dict[str, NetworkServiceInfo]: all_service_info[service_info.address] = service_info self.result['available_network_services'] = all_service_info - self._network_services_by_mac_address = all_service_info + self._network_services_by_mac_address = all_service_info return self._network_services_by_mac_address @@ -913,8 +908,7 @@ def get_network_service_dns_servers(self, service: str) -> list[str]: _, stdout, _ = self.networksetup_cmd(f'networksetup -getdnsservers "{service}"') if "There aren't any" in stdout: return [] - else: - return [line.strip() for line in stdout.splitlines()] + return [line.strip() for line in stdout.splitlines()] def get_network_service_search_domains(self, service: str) -> list[str]: """ @@ -926,8 +920,7 @@ def get_network_service_search_domains(self, service: str) -> list[str]: _, stdout, _ = self.networksetup_cmd(f'networksetup -getsearchdomains "{service}"') if "There aren't any" in stdout: return [] - else: - return [line.strip() for line in stdout.splitlines()] + return [line.strip() for line in stdout.splitlines()] """ Main configuration command methods """ @@ -964,17 +957,16 @@ def validate_config(self) -> None: f'MTU setting ({mtu}) for interface {i} ({port_info.address}) below minimum supported value ' f'for hardware port (supported range: {min}-{max})' ) - elif mtu > max: + if mtu > max: raise ConfigurationError( f'MTU setting ({mtu}) for interface {i} ({port_info.address}) above maximum supported value ' f'for hardware port (supported range: {min}-{max})' ) # Validate hardware settings (all other than MTU are gated on speed being present) - speed = hardware.get('speed') - if speed: + if speed := hardware.get('speed'): supported_media = self.get_valid_port_media_configurations(port_info.name) - media_config = HardwarePortMediaConfig( + media_config = HardwarePortMediaConfig( speed=speed, duplex='half-duplex' if hardware['duplex'] in ['half', 'half-duplex'] else 'full-duplex', flow_control=hardware['flow_control'], @@ -1012,11 +1004,7 @@ def configure_interface(self, config: dict[str, Any]) -> None: elif config['dhcp_with_manual_address']: new_ip_address = config['dhcp_with_manual_address']['ip_address'] self.networksetup_cmd(f'networksetup -setmanualwithdhcprouter "{service.name}" "{new_ip_address}"') - if service.configuration != 'dhcp_with_manual_address': - self.result['changelog'].append( - f'Set service "{service.name}" to DHCP w/ manual address "{new_ip_address}"' - ) - elif service.ip_address != new_ip_address: + if service.configuration != 'dhcp_with_manual_address' or service.ip_address != new_ip_address: self.result['changelog'].append( f'Set service "{service.name}" to DHCP w/ manual address "{new_ip_address}"' ) @@ -1175,7 +1163,7 @@ def run(self) -> None: self.validate_config() # Configure interfaces - for i, interface in enumerate(self.interfaces): + for _, interface in enumerate(self.interfaces): self.configure_interface(interface) diff --git a/library/osx_pmset.py b/library/osx_pmset.py index 20e1973..572f869 100755 --- a/library/osx_pmset.py +++ b/library/osx_pmset.py @@ -106,7 +106,7 @@ def run_module() -> None: commands: list[Any] = [] result: dict[str, Any] = { 'changed': False, - 'diff': { + 'diff': { 'before': '', 'after': '', }, @@ -114,7 +114,7 @@ def run_module() -> None: def add_diff(block: str, param: str, old_value: Any, new_value: Any) -> None: result['diff']['before'] += f'{block}.{param}={old_value}\n' - result['diff']['after'] += f'{block}.{param}={new_value}\n' + result['diff']['after'] += f'{block}.{param}={new_value}\n' blocks: list[tuple[str, str, Any]] = [ ('on_battery', '-b', output['Battery Power']), @@ -124,7 +124,7 @@ def add_diff(block: str, param: str, old_value: Any, new_value: Any) -> None: for param, value in module.params[block].items(): if value is None: continue - elif param not in current_values: + if param not in current_values: module.fail_json( msg=( f'{param} is not present in pmset output. ' diff --git a/local_bootstrap.sh b/local_bootstrap.sh index 3c88966..4475360 100755 --- a/local_bootstrap.sh +++ b/local_bootstrap.sh @@ -12,13 +12,11 @@ set -e # Determine script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOTFILES_DIR="${SCRIPT_DIR}" -HOME_DIR=$HOME -USERNAME=$(whoami) # Validate we're in the right place by checking for required files/directories -if [[ ! -f "${DOTFILES_DIR}/run.py" ]] || \ - [[ ! -f "${DOTFILES_DIR}/local_bootstrap.sh" ]] || \ - [[ ! -d "${DOTFILES_DIR}/playbooks" ]]; then +if [[ ! -f "${DOTFILES_DIR}/run.py" ]] || + [[ ! -f "${DOTFILES_DIR}/local_bootstrap.sh" ]] || + [[ ! -d "${DOTFILES_DIR}/playbooks" ]]; then echo -e "${RED}ERROR: This doesn't appear to be the dotfiles directory!${NC}" echo -e "${RED}Expected to find: run.py, local_bootstrap.sh, playbooks/${NC}" echo -e "${RED}Current directory: ${DOTFILES_DIR}${NC}" diff --git a/nas_bootstrap.sh b/nas_bootstrap.sh index f5aa322..405011a 100755 --- a/nas_bootstrap.sh +++ b/nas_bootstrap.sh @@ -12,13 +12,11 @@ set -e # Determine script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOTFILES_DIR="${SCRIPT_DIR}" -HOME_DIR=$HOME -USERNAME=$(whoami) # Validate we're in the right place by checking for required files/directories -if [[ ! -f "${DOTFILES_DIR}/run.py" ]] || \ - [[ ! -f "${DOTFILES_DIR}/nas_bootstrap.sh" ]] || \ - [[ ! -d "${DOTFILES_DIR}/playbooks" ]]; then +if [[ ! -f "${DOTFILES_DIR}/run.py" ]] || + [[ ! -f "${DOTFILES_DIR}/nas_bootstrap.sh" ]] || + [[ ! -d "${DOTFILES_DIR}/playbooks" ]]; then echo -e "${RED}ERROR: This doesn't appear to be the dotfiles directory!${NC}" echo -e "${RED}Expected to find: run.py, nas_bootstrap.sh, playbooks/${NC}" echo -e "${RED}Current directory: ${DOTFILES_DIR}${NC}" diff --git a/playbooks/README.md b/playbooks/README.md index 1dba2c4..2175552 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -19,12 +19,13 @@ different environments: personal macOS machines and home NAS/server infrastructu The playbook system provides two complete environment bootstraps: -| Playbook | Target | Bootstrap Script | Purpose | -|----------|--------|------------------|---------| -| `local_bootstrap.yml` | macOS (localhost) | `local_bootstrap.sh` | Personal Mac configuration with GUI apps, dev tools, and system settings | -| `nas_bootstrap.yml` | Debian server (nas host) | `nas_bootstrap.sh` | Home NAS with Docker media stack, file sharing, and network services | +| Playbook | Target | Bootstrap Script | Purpose | +| --------------------- | ------------------------ | -------------------- | ------------------------------------------------------------------------ | +| `local_bootstrap.yml` | macOS (localhost) | `local_bootstrap.sh` | Personal Mac configuration with GUI apps, dev tools, and system settings | +| `nas_bootstrap.yml` | Debian server (nas host) | `nas_bootstrap.sh` | Home NAS with Docker media stack, file sharing, and network services | Both playbooks: + - Require inventory from separate git submodule (`inventory/`) - Use encrypted Ansible Vault for secrets - Support selective execution via tags @@ -40,28 +41,29 @@ Configures personal macOS machines with development environment, GUI application **Role execution order:** -1. **zsh** - Shell configuration and plugins - - Tags: `zsh`, `configfile` -2. **macos** - Package manager setup (Homebrew, casks, Mac App Store) - - Tags: `macos` -3. **macos_settings** - System preferences (Finder, Activity Monitor, etc.) - - Tags: `macos`, `dock` -4. **macos_dock** - Dock configuration and layout - - Tags: `macos`, `dock` -5. **git** - Git configuration and gh CLI - - Tags: `git`, `configfile` -6. **hammerspoon** - Window management automation - - Tags: `hammerspoon`, `configfile` -7. **iterm** - iTerm2 terminal configuration - - Tags: `iterm` -8. **ssh** - SSH client configuration - - Tags: `ssh`, `configfile` -9. **starship** - Shell prompt configuration - - Tags: `starship` +01. **zsh** - Shell configuration and plugins + - Tags: `zsh`, `configfile` +02. **macos** - Package manager setup (Homebrew, casks, Mac App Store) + - Tags: `macos` +03. **macos_settings** - System preferences (Finder, Activity Monitor, etc.) + - Tags: `macos`, `dock` +04. **macos_dock** - Dock configuration and layout + - Tags: `macos`, `dock` +05. **git** - Git configuration and gh CLI + - Tags: `git`, `configfile` +06. **hammerspoon** - Window management automation + - Tags: `hammerspoon`, `configfile` +07. **iterm** - iTerm2 terminal configuration + - Tags: `iterm` +08. **ssh** - SSH client configuration + - Tags: `ssh`, `configfile` +09. **starship** - Shell prompt configuration + - Tags: `starship` 10. **python** - Python tooling (pipx, uv, hatch) - Tags: `python` **Variables required:** + - `admin_password` (from `inventory/group_vars/all/vault.yml` or `inventory/group_vars/localhost/vault.yml`) **Common usage examples:** @@ -110,6 +112,7 @@ Configures home NAS server running Debian with Docker media stack, ZFS storage, - Tags: `zsh`, `configfile` **Variables required:** + - `admin_password` (from `inventory/group_vars/all/vault.yml` or `inventory/host_vars/nas/vault.yml`) **Common usage examples:** @@ -145,6 +148,7 @@ chmod u+x nas_bootstrap.sh ``` **What the scripts do:** + 1. Validate repository structure (checks for `run.py`, playbooks, etc.) 2. Install Homebrew with checksum verification (if not present) 3. Install Ansible via Homebrew (if not present) @@ -263,14 +267,16 @@ fi ``` **Why:** Protects against: + - Man-in-the-middle attacks - DNS hijacking - Compromised download sources **When checksum changes:** + 1. Verify new installer at https://github.com/Homebrew/install 2. Generate new checksum: -`curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | shasum -a 256` + `curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | shasum -a 256` 3. Update `EXPECTED_CHECKSUM` in both bootstrap scripts 4. Update "Last updated" comment @@ -287,26 +293,31 @@ set -e # Exit immediately on any command failure **Both scripts follow this pattern:** 1. **Setup** + - Define color codes for output - Enable fail-fast (`set -e`) - Determine script directory 2. **Validation** + - Check for required files/directories - Exit with error if validation fails 3. **Dependency Installation** + - Install Homebrew if missing (with checksum verification) - Add Homebrew to PATH - Install Ansible via Homebrew if missing 4. **Playbook Execution** + - Run corresponding playbook with: - Inventory file: `inventory/inventory.yml` - Become password prompt: `--ask-become-pass` - Verbose output: `-vv` 5. **Post-Bootstrap** (macOS only) + - Kick off macOS software update - Note: System may restart automatically @@ -314,10 +325,10 @@ set -e # Exit immediately on any command failure ### Tag Categories -**Role-specific tags:** -Each role has its own tag matching the role name (e.g., `zsh`, `git`, `macos`). +**Role-specific tags:** Each role has its own tag matching the role name (e.g., `zsh`, `git`, `macos`). **Functional tags:** + - `configfile` - All roles that create/link configuration files - `macos` - All macOS-specific roles (app installation + settings) - `dock` - Dock-related configuration (settings + layout) @@ -331,13 +342,13 @@ Each role has its own tag matching the role name (e.g., `zsh`, `git`, `macos`). Roles can have multiple tags for flexible targeting: ```yaml -- include_role: - name: roles/zsh - apply: - tags: zsh - tags: - - zsh - - configfile + - include_role: + name: roles/zsh + apply: + tags: zsh + tags: + - zsh + - configfile ``` This role runs when either `zsh` OR `configfile` tags are specified. @@ -368,6 +379,7 @@ This role runs when either `zsh` OR `configfile` tags are specified. **All secrets are encrypted using Ansible Vault:** 1. Create `vault_password.txt` in repository root (gitignored): + ```bash echo "your-vault-password" > vault_password.txt chmod 600 vault_password.txt @@ -376,11 +388,13 @@ This role runs when either `zsh` OR `configfile` tags are specified. 2. Ansible automatically uses this file (configured in `ansible.cfg` or inventory) 3. Encrypted vault files in inventory: + - `inventory/group_vars/all/vault.yml` - Shared secrets - `inventory/group_vars/localhost/vault.yml` - Mac-specific secrets - `inventory/host_vars/nas/vault.yml` - NAS-specific secrets **Never commit:** + - `vault_password.txt` - Decrypted vault contents - Plain-text passwords or API keys @@ -390,15 +404,16 @@ This role runs when either `zsh` OR `configfile` tags are specified. Both playbooks set `ansible_become_pass` from vault at the start: ```yaml -- name: Set ansible_become_pass - ansible.builtin.set_fact: - ansible_become_pass: "{{ admin_password }}" - no_log: true # Prevents password from appearing in logs - tags: - - always + - name: Set ansible_become_pass + ansible.builtin.set_fact: + ansible_become_pass: '{{ admin_password }}' + no_log: true # Prevents password from appearing in logs + tags: + - always ``` **Benefits:** + - Only one password prompt (via `--ask-become-pass`) - Password available to all roles for privilege escalation - Not logged to output (even with `-vvv`) @@ -408,16 +423,19 @@ Both playbooks set `ansible_become_pass` from vault at the start: For remote playbooks (like `nas_bootstrap.yml`): 1. Ensure SSH key access to target host: + ```bash ssh-copy-id user@nas-hostname ``` 2. Verify connection: + ```bash ansible nas -i inventory/inventory.yml -m ping ``` 3. If using custom SSH keys, configure in `inventory/inventory.yml`: + ```yaml nas: ansible_host: 192.168.1.100 @@ -430,12 +448,13 @@ For remote playbooks (like `nas_bootstrap.yml`): ### Inventory Not Found **Symptom:** + ``` ERROR! the playbook could not be found ``` -**Solution:** -Update inventory submodule: +**Solution:** Update inventory submodule: + ```bash python3 run.py update-inventory # Or manually: @@ -445,11 +464,13 @@ git submodule update --init --recursive ### Vault Password Issues **Symptom:** + ``` ERROR! Attempting to decrypt but no vault secrets found ``` **Solution:** + 1. Verify `vault_password.txt` exists in repository root 2. Ensure file contains correct password 3. Check file permissions: `chmod 600 vault_password.txt` @@ -458,11 +479,13 @@ ERROR! Attempting to decrypt but no vault secrets found ### Homebrew Checksum Mismatch **Symptom:** + ``` WARNING: Homebrew installer checksum mismatch! ``` **Solution:** + 1. Verify if Homebrew installer was legitimately updated: - Check https://github.com/Homebrew/install for recent commits 2. Generate new checksum: @@ -476,12 +499,14 @@ WARNING: Homebrew installer checksum mismatch! ### Role Failures **Symptom:** + ``` TASK [roles/some-role : Some task] ***************************************** fatal: [localhost]: FAILED! => {...} ``` **Solution:** + 1. Check verbose output for specific error 2. Run specific role with extra verbosity: ```bash @@ -496,10 +521,10 @@ fatal: [localhost]: FAILED! => {...} ### macOS Dock Configuration Not Applying -**Symptom:** -Dock settings changed by playbook revert after restart +**Symptom:** Dock settings changed by playbook revert after restart **Solution:** + 1. Kill Dock to force reload: ```bash killall Dock @@ -509,10 +534,10 @@ Dock settings changed by playbook revert after restart ### Docker Stack Issues (NAS) -**Symptom:** -Docker containers not starting or configuration not applied +**Symptom:** Docker containers not starting or configuration not applied **Solution:** + 1. Check Docker service status: ```bash ssh nas "sudo systemctl status docker" @@ -533,11 +558,13 @@ Docker containers not starting or configuration not applied ### Permission Denied Errors **Symptom:** + ``` fatal: [localhost]: FAILED! => {"changed": false, "msg": "Permission denied"} ``` **Solution:** + 1. Verify `--ask-become-pass` flag is used 2. Check become password is correct in vault 3. Ensure user has sudo privileges @@ -555,6 +582,7 @@ fatal: [localhost]: FAILED! => {"changed": false, "msg": "Permission denied"} ### Development Workflow 1. **Test with specific tags:** + ```bash ansible-playbook playbooks/local_bootstrap.yml \ -i inventory/inventory.yml \ @@ -565,6 +593,7 @@ fatal: [localhost]: FAILED! => {"changed": false, "msg": "Permission denied"} ``` 2. **Use check mode for validation:** + ```bash ansible-playbook playbooks/local_bootstrap.yml \ -i inventory/inventory.yml \ @@ -573,6 +602,7 @@ fatal: [localhost]: FAILED! => {"changed": false, "msg": "Permission denied"} ``` 3. **Run full bootstrap periodically:** + - Weekly for active development machines - Monthly for stable production systems - After major OS updates @@ -582,13 +612,13 @@ fatal: [localhost]: FAILED! => {"changed": false, "msg": "Permission denied"} 1. Create role in `roles/` directory 2. Add to appropriate playbook: ```yaml - - include_role: - name: roles/new-role - apply: - tags: new-role - tags: - - new-role - - configfile # If it creates config files + - include_role: + name: roles/new-role + apply: + tags: new-role + tags: + - new-role + - configfile # If it creates config files ``` 3. Test with tag: ```bash diff --git a/pyproject.toml b/pyproject.toml index a7655eb..dcf3b76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,53 +1,129 @@ -#################### -# Project metadata # -#################### +# #################################################################################################################### +# Project metadata +# #################################################################################################################### [project] -name = "connor-mason-dotfiles" -version = "0.0.1" +name = "dotfiles" +version = "0.0.1" description = "Connor's dotfiles" +readme = "README.md" +authors = [{name = "Connor Mason"}] +keywords = ["dotfiles", "config", "configuration", "shell"] +classifiers = [ + # keep-sorted start + "Development Status :: 4 - Beta", + "Environment :: Console", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: MacOS", + "Programming Language :: Python", + "Topic :: Software Development", + "Typing :: Typed", + # keep-sorted end +] # Project requirements # ==================== -requires-python = ">= 3.10" -dependencies = [ +requires-python = ">= 3.12" +dependencies = [ + # keep-sorted start case=no + "ansible-core", + "PyYaml >= 6.0", "typing-extensions", + # keep-sorted end ] # Project URLs # ============ [project.urls] -Homepage = "https://github.com/connormason/dotclaude" -Repository = "https://github.com/connormason/dotclaude" -Source = "https://github.com/connormason/dotclaude" +Homepage = "https://github.com/connormason/dotfiles" +Repository = "https://github.com/connormason/dotfiles" +Source = "https://github.com/connormason/dotfiles" # Project dependency groups # ========================= [dependency-groups] dev = [ {include-group = "lint"}, + {include-group = "test"}, ] lint = [ + # keep-sorted start case=no + "codespell", + "flake8-dunder-all", "interrogate >= 1.7.0", + "mdformat", + "mdformat-config", + "mdformat-footnote", + "mdformat-front-matters", + "mdformat-gfm", + "mdformat-gfm-alerts", + "mdformat-pyproject", + "mdformat-simple-breaks", + "mdformat-toc", + "mypy >= 1.19.1, < 1.20 ; python_version < '3.10'", + "mypy >= 2.0 ; python_version >= '3.10'", + "prek", + "pyjson5", "ruff >= 0.15.8", - "mypy >= 1.19.1", - "pre-commit", + "shellcheck-py", + "shfmt_py", + "taplo", "types-PyYaml", "types-requests", + "types-toml", + "validate-pyproject-schema-store", + "validate-pyproject[all]", + "yamllint", + # keep-sorted end +] +test = [ + # keep-sorted start case=no + "coverage[toml] >= 7.11", + "pytest >= 9.0", + "pytest-clarity >= 1.0.1", # Better assertion diffs + "pytest-html >= 3.2.0", # HTML test reports + "pytest-mock >= 3.11.1", # Mocking support + "pytest-sugar >= 0.9.7", # Better pytest output + "pytest-timeout >= 2.1.0", # Test timeouts + "pytest-xdist >= 3.3.0", # Parallel test execution + "pyyaml >= 6.0.1", # YAML parsing + "yamllint >= 1.32.0", # Ansible testing + # keep-sorted end ] -# #################### -# Tool configuration # -# #################### +# #################################################################################################################### +# Package management tool configuration +# #################################################################################################################### + +# `uv` configuration +# ================== +[tool.uv] +default-groups = ["dev"] + +[[tool.uv.index]] +name = "default" +url = "https://pypi.org/simple" +default = true + +# #################################################################################################################### +# Linting tool configuration +# #################################################################################################################### # `codespell` configuration +# ========================= [tool.codespell] -count = true -summary = true -skip = "smb.conf.j2" +count = true +summary = true +skip = "smb.conf.j2" ignore-words-list = [ + # keep-sorted start + "*.css", + "*.html", "hass", "iterm", + "wit", + # keep-sorted end ] # `interrogate` configuration @@ -66,11 +142,21 @@ ignore-private = true ignore-property-decorators = false ignore-semiprivate = true ignore-setters = true + +# NOTE: This exclude list not currently considered when `interrogate` is run with pre-commit (prek) hooks. +# Excluded files are duplicated in .pre-commit-config.yaml exclude = [ + # keep-sorted start + ".cicd/", + ".claude/", + ".nox/", ".tmp/", + ".worktrees/", + "archive/", "build/", "docs/", "tests/", + # keep-sorted end ] # `mypy` configuration @@ -79,85 +165,308 @@ exclude = [ disallow_untyped_defs = true exclude_gitignore = true ignore_missing_imports = true -python_version = "3.9" +python_version = "3.9" # TODO: update warn_unused_configs = true exclude = [ - "services/homeassistant/" + "services/homeassistant/", ] -# `pytest` configuration -# ====================== -[tool.pytest.ini_options] -testpaths = ["tests"] +[[tool.mypy.overrides]] +disable_error_code = ["override"] +module = ["run"] -# `ruff` configuration -# ==================== +# `ruff` global configuration +# =========================== [tool.ruff] -line-length = 120 -indent-width = 4 -target-version = "py39" +indent-width = 4 +line-length = 120 +output-format = "full" +respect-gitignore = true +show-fixes = true +target-version = "py39" + extend-exclude = [ "docs/", "services/homeassistant/config/custom_components/", ] +# `ruff check` linter configuration +# ================================= [tool.ruff.lint] -typing-extensions = true -typing-modules = ["typing", "types", "typing_extensions"] +future-annotations = true +typing-extensions = true +typing-modules = ["typing", "types", "typing_extensions"] + +exclude = [ + "archive/**/*.py", +] +extend-safe-fixes = [ + # keep-sorted start + "B007", # Loop control variable{name}not used within loop body + "B028", # No explicit `stacklevel` keyword argument found + "D400", # First line should end with a period + # keep-sorted end +] extend-select = [ - "ANN", # flake8-annotations - "C4", # flake8-comprehensions - "COM", # flake8-commas - "EXE", # flake8-executable - "FA", # flake8-future-imports - "I", # isort - "ICN", # flake8-import-convetions - "INP", # flake8-no-pep420 - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "PGH", # pygrep-hooks - "PT", # flake8-pytest-style - "PTH", # flake8-use-pathlib - "Q", # flake8-quotes - "RSE", # flake8-raise - "RUF", # ruff - "TC", # flake8-type-checking - "TID", # flake8-tidy-imports - "UP", # pyupgrade - "W", # pycodestyle warnings + # keep-sorted start + "ANN", # flake8-annotations + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "COM", # flake8-commas + "E", # pycodestyle errors [E4, E7, E9 included by default (lint.select)] + "EXE", # flake8-executable + "F", # pyflakes [included by default (lint.select)] + "FA", # flake8-future-imports + "FURB", # refurb + "I", # isort + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "PERF", # perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PLC", # Pylint - Convention + "PLE", # Pylint - Error + "PLR", # Pylint - Refactor + "PLW", # Pylint - Warning + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # ruff + "S", # flake8-bandit + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "T10", # flake8-debugger + "TC", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 + # keep-sorted end ] ignore = [ - "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} - "C408", # Unnecessary {kind}() call (rewrite as a literal) - "COM812", # Trailing comma missing - "PT011", # pytest.raises({exception}) is too broad, set the match parameter or use a more specific exception - "PT012", # pytest.raises() block should contain a single simple statement - "PT017", # Found assertion on exception {name} in except block, use pytest.raises() instead - "RUF022", # `__all__` is not sorted + # flake8-annotations ---------------------------------------------------------------------------------------------- + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} + # flake8-bugbear -------------------------------------------------------------------------------------------------- + "B009", # Do not call `getattr` with a constant attribute value + "B010", # Do not call `setattr` with a constant attribute value + # flake8-comprehensions ------------------------------------------------------------------------------------------- + "C408", # Unnecessary {kind}() call (rewrite as a literal) + # flake8-commas --------------------------------------------------------------------------------------------------- + "COM812", # Trailing comma missing + # perflint -------------------------------------------------------------------------------------------------------- + "PERF203", # `try`-`except` within a loop incurs performance overhead + # flake8-pie ------------------------------------------------------------------------------------------------------ + "PIE790", # Unnecessary `pass` statement + "PIE796", # Enum contains duplicate value: `{value}` + # Pylint - Convention --------------------------------------------------------------------------------------------- + "PLC0414", # Import alias does not rename original package + "PLC0415", # `import` should be at the top-level of a file + # Pylint - Refactor ----------------------------------------------------------------------------------------------- + "PLR0911", # Too many return statements + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments in function definition ({c_args} > {max_args}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + # Pylint - Warning ------------------------------------------------------------------------------------------------ + "PLW0603", # Using the global statement to update {name} is discouraged + "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target + # flake8-pytest-style --------------------------------------------------------------------------------------------- + "PT011", # pytest.raises({exception}) is too broad, set the match parameter or use a more specific exception + "PT012", # pytest.raises() block should contain a single simple statement + "PT017", # Found assertion on exception {name} in except block, use pytest.raises() instead + # flake8-pyi ------------------------------------------------------------------------------------------------------ + "PYI051", # `Literal[{literal}]` is redundant in a union with `{builtin_type}` + # flake8-bandit --------------------------------------------------------------------------------------------------- + "S108", # Probable insecure usage of temporary file or directory: "{}" + "S110", # `try`-`except`-`pass` detected, consider logging the exception + "S603", # `subprocess` call: check for execution of untrusted input + "S607", # Starting a process with a partial executable path + "S608", # Possible SQL injection vector through string-based query construction + # flake8-simplify ------------------------------------------------------------------------------------------------- + "SIM118", # Use `key {operator} dict` instead of `key {operator} dict.keys()` + + # TODO: ?? + # "RUF022", # `__all__` is not sorted ] + +# `ruff check` linter per-file rule ignores +# ========================================= [tool.ruff.lint.per-file-ignores] -"scripts/env/venvshell.py" = ["RUF001"] # String contains ambiguous {}. Did you mean {}? +# keep-sorted start +"run.py" = ["S112"] +"scripts/env/venvshell.py" = ["RUF001"] +"services/homeassistant/config/scripts/get_lutron_cert.py" = ["S"] +"tests/**" = ["B", "S", "SIM", "PERF"] +# keep-sorted end -[tool.ruff.lint.isort] -force-single-line = true -required-imports = ["from __future__ import annotations"] +# `ruff check` linter rule group-specific settings +# ================================================ +# "flake8-annotations" lint rule settings [ANN] +# --------------------------------------------- +[tool.ruff.lint.flake8-annotations] +suppress-dummy-args = true +# "flake8-quotes" lint rule settings [Q] +# -------------------------------------- [tool.ruff.lint.flake8-quotes] -inline-quotes = "single" +docstring-quotes = "double" +inline-quotes = "single" +multiline-quotes = "double" +# "flake8-type-checking" lint rule settings [TC] +# ---------------------------------------------- [tool.ruff.lint.flake8-type-checking] -exempt-modules = ["typing", "typing_extensions"] +exempt-modules = ["collections.abc", "typing", "typing_extensions"] runtime-evaluated-base-classes = ["pydantic.BaseModel"] +runtime-evaluated-decorators = [] + +# "isort" lint rule settings [I] +# ------------------------------ +[tool.ruff.lint.isort] +force-single-line = true +required-imports = ["from __future__ import annotations"] + +# "pycodestyle" lint rule settings [E, W] +# --------------------------------------- +[tool.ruff.lint.pycodestyle] +ignore-overlong-task-comments = true +max-doc-length = 120 +max-line-length = 120 + +# "pydocstyle" lint rule settings [D] +# ----------------------------------- +[tool.ruff.lint.pydocstyle] +convention = "numpy" +ignore-decorators = ["typing.overload"] +property-decorators = [] + +# "pylint" lint rule settings [PLC, PLE, PLR, PLW] +# ------------------------------------------------ +[tool.ruff.lint.pylint] +allow-magic-value-types = ["str", "bytes", "int", "float"] # PLR2004 +# "pyupgrade" lint rule settings [UP] +# ----------------------------------- [tool.ruff.lint.pyupgrade] keep-runtime-typing = true -# `uv` configuration -# ================== -[[tool.uv.index]] -name = "default" -url = "https://pypi.org/simple" -default = true + +# #################################################################################################################### +# Formatting tool configuration +# #################################################################################################################### + +# `mdformat` configuration +# ======================== +[tool.mdformat] +end_of_line = "lf" # options: {"lf", "crlf", "keep"} +number = true # options: {false, true} +validate = true # options: {false, true} +wrap = 120 # options: {"keep", "no", INTEGER} + +# `ruff format` configuration +# =========================== +[tool.ruff.format] +docstring-code-format = true +exclude = [".gitignore"] +indent-style = "space" +quote-style = "single" + + +# #################################################################################################################### +# Testing tool configuration +# #################################################################################################################### + +# `pytest` configuration +# ====================== +[tool.pytest.ini_options] +minversion = "3.11" +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] + +junit_family = "xunit2" +render_collapsed = "all" + +addopts = """ +-v +-l +-ra +--strict-markers +-W default +-p no:rerunfailures +--html=.reports/pytest.html +--junit-xml=.reports/pytest.xml +""" + +markers = [ + # keep-sorted start + "macos_only: Tests that only run on macOS", + "requires_homebrew: Tests that require Homebrew to be installed", + "role: Role-specific tests", + "slow: Tests that take significant time", + # keep-sorted end +] +norecursedirs = [ + # keep-sorted start + "*.egg", + ".git", + ".tox", + "__pycache__", + "archive", + "build", + "dist", + "inventory", + # keep-sorted end +] + +# `coverage` configuration +# ======================== +[tool.coverage.run] +source = ["."] +omit = [ + # keep-sorted start + "*/dist-packages/*", + "*/migrations/*", + "*/site-packages/*", + ".venv/*", + "archive/*", + "inventory/*", + "setup.py", + "tests/*", + "venv/*", + # keep-sorted end +] + +[tool.coverage.html] +directory = ".reports/coverage" + +[tool.coverage.json] +output = ".reports/coverage.json" + +[tool.coverage.report] +skip_covered = false +show_empty = true +show_missing = true +precision = 2 +fail_under = 70 +exclude_lines = [ + # keep-sorted start + "def __repr__", + "if TYPE_CHECKING:", + "if __name__ == .__main__.:", + "if self\\.debug", + "if typing.TYPE_CHECKING:", + "pass", + "pragma: no cover", + "raise AssertionError", + "raise NotImplementedError", + # keep-sorted end +] diff --git a/requirements.txt b/requirements.txt index 5f1a1db..9375ab6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,186 @@ -ansible -pre-commit +ansible-core==2.21.1 + # via dotfiles +ast-serialize==0.6.0 + # via mypy +astatine==0.3.3 + # via flake8-dunder-all +asttokens==3.0.1 + # via astatine +attrs==26.1.0 + # via interrogate +cffi==2.1.0 ; platform_python_implementation != 'PyPy' + # via cryptography +click==8.4.2 + # via + # consolekit + # flake8-dunder-all + # interrogate +codespell==2.4.2 +colorama==0.4.6 + # via + # click + # interrogate + # pytest +consolekit==1.13.0 + # via flake8-dunder-all +coverage==7.15.0 +cryptography==49.0.0 + # via ansible-core +deprecation==2.1.0 + # via deprecation-alias +deprecation-alias==0.4.0 + # via consolekit +domdf-python-tools==3.10.0 + # via + # astatine + # consolekit + # flake8-dunder-all +execnet==2.1.2 + # via pytest-xdist +fastjsonschema==2.21.2 + # via validate-pyproject +flake8==7.3.0 + # via flake8-dunder-all +flake8-dunder-all==0.5.0 +iniconfig==2.3.0 + # via pytest +interrogate==1.7.0 +jinja2==3.1.6 + # via + # ansible-core + # pytest-html +librt==0.12.0 ; platform_python_implementation != 'PyPy' + # via mypy +markdown-it-py==4.2.0 + # via + # mdformat + # mdformat-gfm + # mdit-py-plugins + # rich +markupsafe==3.0.3 + # via jinja2 +mccabe==0.7.0 + # via flake8 +mdformat==1.0.0 + # via + # mdformat-config + # mdformat-footnote + # mdformat-front-matters + # mdformat-gfm + # mdformat-gfm-alerts + # mdformat-pyproject + # mdformat-simple-breaks + # mdformat-toc +mdformat-config==0.2.1 +mdformat-footnote==0.1.3 +mdformat-front-matters==2.0.0 +mdformat-gfm==1.0.0 +mdformat-gfm-alerts==2.0.0 +mdformat-pyproject==0.1.1 +mdformat-simple-breaks==0.1.0 +mdformat-toc==0.5.0 +mdit-py-plugins==0.6.1 + # via + # mdformat-footnote + # mdformat-front-matters + # mdformat-gfm + # mdformat-gfm-alerts +mdurl==0.1.2 + # via markdown-it-py +mistletoe==1.5.1 + # via consolekit +mypy==2.2.0 +mypy-extensions==1.1.0 + # via mypy +natsort==8.4.0 + # via + # domdf-python-tools + # flake8-dunder-all +packaging==26.2 + # via + # ansible-core + # deprecation + # deprecation-alias + # pytest + # validate-pyproject +pathspec==1.1.1 + # via + # mypy + # yamllint +pluggy==1.6.0 + # via pytest +pprintpp==0.4.0 + # via pytest-clarity +prek==0.4.8 +py==1.11.0 + # via interrogate +pycodestyle==2.14.0 + # via flake8 +pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' + # via cffi +pyflakes==3.4.0 + # via flake8 +pygments==2.20.0 + # via + # pytest + # rich +pyjson5==2.0.1 +pytest==9.1.1 + # via + # pytest-clarity + # pytest-html + # pytest-metadata + # pytest-mock + # pytest-sugar + # pytest-timeout + # pytest-xdist +pytest-clarity==1.0.1 +pytest-html==4.2.0 +pytest-metadata==3.1.1 + # via pytest-html +pytest-mock==3.15.1 +pytest-sugar==1.1.1 +pytest-timeout==2.4.0 +pytest-xdist==3.8.0 +pyyaml==6.0.3 + # via + # ansible-core + # dotfiles + # yamllint +resolvelib==1.2.1 + # via ansible-core +rich==15.0.0 + # via pytest-clarity +ruamel-yaml==0.19.1 + # via + # mdformat-config + # mdformat-front-matters +ruff==0.15.20 +shellcheck-py==0.11.0.1 +shfmt-py==4.0.0 +tabulate==0.10.0 + # via interrogate +taplo==0.9.3 + # via mdformat-config +termcolor==3.3.0 + # via pytest-sugar +toml==0.10.2 + # via mdformat-front-matters +trove-classifiers==2026.6.1.19 + # via validate-pyproject +types-pyyaml==6.0.12.20260518 +types-requests==2.33.0.20260518 +types-toml==0.10.8.20260518 +typing-extensions==4.16.0 + # via + # consolekit + # domdf-python-tools + # dotfiles + # mypy +urllib3==2.7.0 + # via types-requests +validate-pyproject==0.25 +validate-pyproject-schema-store==2026.7.8 +wcwidth==0.8.2 + # via mdformat-gfm +yamllint==1.38.0 diff --git a/roles/README.md b/roles/README.md index c831a9c..75f1894 100644 --- a/roles/README.md +++ b/roles/README.md @@ -1,11 +1,12 @@ # Ansible Roles -This directory contains Ansible roles for automated configuration of macOS and Debian Linux systems. Roles are -organized by platform and provide modular, reusable configuration components for the dotfiles repository. +This directory contains Ansible roles for automated configuration of macOS and Debian Linux systems. Roles are organized +by platform and provide modular, reusable configuration components for the dotfiles repository. ## Overview The role system provides: + - **Platform-specific roles** for macOS and Linux system configuration - **Shared roles** for cross-platform tools and configurations - **Utility roles** for common operations like dotfile linking @@ -19,39 +20,39 @@ Roles are executed by playbooks in `playbooks/` and can be selectively applied u Platform-specific roles for macOS configuration: -| Role | Description | Key Features | -|------|-------------|--------------| -| **macos** | Core macOS package management | Homebrew formulae (30+ CLI tools), cask apps (GUI applications), Mac App Store apps via `mas` CLI | -| **macos_settings** | System preferences configuration | Finder, Dock, Activity Monitor, Messages, power management, I/O devices via `defaults write` | -| **macos_dock** | Dock icon configuration | Manages Dock items and layout | -| **hammerspoon** | Window management automation | Configures Hammerspoon app for macOS window management | -| **iterm** | Terminal configuration | iTerm2 application setup and preferences | -| **python** | Python tooling for macOS | Global pip packages, pipx packages | +| Role | Description | Key Features | +| ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------- | +| **macos** | Core macOS package management | Homebrew formulae (30+ CLI tools), cask apps (GUI applications), Mac App Store apps via `mas` CLI | +| **macos_settings** | System preferences configuration | Finder, Dock, Activity Monitor, Messages, power management, I/O devices via `defaults write` | +| **macos_dock** | Dock icon configuration | Manages Dock items and layout | +| **hammerspoon** | Window management automation | Configures Hammerspoon app for macOS window management | +| **iterm** | Terminal configuration | iTerm2 application setup and preferences | +| **python** | Python tooling for macOS | Global pip packages, pipx packages | ### Linux Roles Platform-specific roles for Debian Linux systems: -| Role | Description | Key Features | -|------|-------------|--------------| -| **debian** | Base Debian system setup | System packages and configuration | +| Role | Description | Key Features | +| ---------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| **debian** | Base Debian system setup | System packages and configuration | | **docker** | Docker containerization platform | Docker engine, Caddy reverse proxy, Autoplex/PyAutoplex image builds, Home Assistant, 195-line docker-compose stack | -| **zfs** | ZFS filesystem support | ZFS dependencies and configuration | -| **samba** | File sharing | Samba server configuration for network file sharing | -| **glance** | Dashboard application | [Glance](https://github.com/glanceapp/glance) dashboard setup | +| **zfs** | ZFS filesystem support | ZFS dependencies and configuration | +| **samba** | File sharing | Samba server configuration for network file sharing | +| **glance** | Dashboard application | [Glance](https://github.com/glanceapp/glance) dashboard setup | ### Shared Roles Cross-platform roles that work on both macOS and Linux: -| Role | Description | Key Features | -|------|-------------|--------------| -| **git** | Git version control | Git configuration, gh CLI tool | -| **ssh** | SSH client/server | Key generation for remote hosts, ssh-agent loading for macOS, GitHub key authorization | -| **zsh** | Shell configuration | oh-my-zsh installation, Powerlevel10k theme, `.zshrc`/`.zprofile` setup, default shell configuration | -| **starship** | Shell prompt | Starship prompt installation and configuration (alternative to Powerlevel10k) | -| **link_dotfile** | Dotfile linking utility | Reusable role for safely creating symlinks with validation and backup | -| **tailscale** | VPN networking | [Tailscale VPN](https://tailscale.com/) setup using [artis3n.tailscale](https://github.com/artis3n/ansible-role-tailscale) role | +| Role | Description | Key Features | +| ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| **git** | Git version control | Git configuration, gh CLI tool | +| **ssh** | SSH client/server | Key generation for remote hosts, ssh-agent loading for macOS, GitHub key authorization | +| **zsh** | Shell configuration | oh-my-zsh installation, Powerlevel10k theme, `.zshrc`/`.zprofile` setup, default shell configuration | +| **starship** | Shell prompt | Starship prompt installation and configuration (alternative to Powerlevel10k) | +| **link_dotfile** | Dotfile linking utility | Reusable role for safely creating symlinks with validation and backup | +| **tailscale** | VPN networking | [Tailscale VPN](https://tailscale.com/) setup using [artis3n.tailscale](https://github.com/artis3n/ansible-role-tailscale) role | ## Role Dependencies @@ -60,15 +61,18 @@ Cross-platform roles that work on both macOS and Linux: External roles and collections are managed in `roles/requirements.yml`: **External Roles:** + - `elliotweiser.osx-command-line-tools` - macOS command-line tools installation - `artis3n.tailscale` - Tailscale VPN setup **Ansible Collections:** + - `ansible.posix` - POSIX-specific modules - `community.general` - Community-maintained general modules (includes `pipx` module) - `geerlingguy.mac` - macOS-specific modules (Homebrew, Mac App Store) Install external dependencies: + ```bash ansible-galaxy install -r roles/requirements.yml ``` @@ -78,24 +82,29 @@ ansible-galaxy install -r roles/requirements.yml Some roles depend on other roles in this repository: **macos role:** + - Uses `elliotweiser.osx-command-line-tools` for Xcode CLI tools - Uses `geerlingguy.mac.homebrew` for package management - Uses `geerlingguy.mac.mas` for App Store apps **python role:** + - Requires `macos` role to install `pipx` via Homebrew (macOS) - Uses pipx executable from Homebrew prefix **zsh role:** + - On macOS: Uses `elliotweiser.osx-command-line-tools` and `geerlingguy.mac.homebrew` - Installs oh-my-zsh and Powerlevel10k theme - Different behavior for localhost (symlinks) vs. remote hosts (copies files) **starship role:** + - Uses `link_dotfile` role to symlink configuration - Alternative to Powerlevel10k prompt in zsh **docker role:** + - Linux only - Sets up complete Docker infrastructure including custom image builds - Clones and builds multiple custom images (Caddy, Autoplex, PyAutoplex) @@ -109,16 +118,16 @@ Some roles depend on other roles in this repository: Roles are included in playbooks using `include_role`: ```yaml -- name: Personal Mac bootstrap - hosts: localhost - tasks: - - include_role: - name: roles/git - apply: - tags: git - tags: - - git - - configfile + - name: Personal Mac bootstrap + hosts: localhost + tasks: + - include_role: + name: roles/git + apply: + tags: git + tags: + - git + - configfile ``` ### Running Specific Roles @@ -160,15 +169,19 @@ ansible-playbook test_role.yml ### Common Playbook Patterns **Local Mac Bootstrap** (`playbooks/local_bootstrap.yml`): + ```bash chmod u+x local_bootstrap.sh && ./local_bootstrap.sh ``` + Runs: zsh, macos, macos_settings, macos_dock, git, hammerspoon, iterm, ssh, starship, python **NAS Server Bootstrap** (`playbooks/nas_bootstrap.yml`): + ```bash chmod u+x nas_bootstrap.sh && ./nas_bootstrap.sh ``` + Runs: debian, zfs, samba, docker, glance, git, ssh, zsh ## The link_dotfile Pattern @@ -187,40 +200,40 @@ locations. This is the **preferred method** for managing dotfile symlinks across ### Basic Usage ```yaml -- name: Link git config - ansible.builtin.include_role: - name: roles/link_dotfile - vars: - link_dotfile_src: "{{ dotfiles_home }}/roles/git/files/gitconfig" - link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.gitconfig" + - name: Link git config + ansible.builtin.include_role: + name: roles/link_dotfile + vars: + link_dotfile_src: '{{ dotfiles_home }}/roles/git/files/gitconfig' + link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.gitconfig" ``` ### Required Variables -| Variable | Description | Example | -|----------|-------------|---------| -| `link_dotfile_src` | Absolute path to source file in dotfiles repo | `{{ dotfiles_home }}/roles/git/files/gitconfig` | -| `link_dotfile_dst` | Absolute path to destination (target location) | `{{ ansible_facts['user_dir'] }}/.gitconfig` | +| Variable | Description | Example | +| ------------------ | ---------------------------------------------- | ----------------------------------------------- | +| `link_dotfile_src` | Absolute path to source file in dotfiles repo | `{{ dotfiles_home }}/roles/git/files/gitconfig` | +| `link_dotfile_dst` | Absolute path to destination (target location) | `{{ ansible_facts['user_dir'] }}/.gitconfig` | ### Optional Variables -| Variable | Description | Default | -|----------|-------------|---------| +| Variable | Description | Default | +| ------------------ | ------------------------------------------ | --------------------- | | `dotfile_dir_mode` | Permissions for created parent directories | `omit` (uses default) | ### Advanced Example: Linking Multiple Files ```yaml -- name: Link shell dotfiles - ansible.builtin.include_role: - name: roles/link_dotfile - vars: - link_dotfile_src: "{{ dotfiles_home }}/roles/shell/files/{{ item }}" - link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.{{ item }}" - loop: - - zshrc - - zprofile - - zshenv + - name: Link shell dotfiles + ansible.builtin.include_role: + name: roles/link_dotfile + vars: + link_dotfile_src: '{{ dotfiles_home }}/roles/shell/files/{{ item }}' + link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.{{ item }}" + loop: + - zshrc + - zprofile + - zshenv ``` ### Real-World Example: Starship Configuration @@ -228,22 +241,23 @@ locations. This is the **preferred method** for managing dotfile symlinks across From `roles/starship/tasks/main.yaml`: ```yaml -- name: Configure Starship - tags: - - configfile - - preferences - block: - - name: Link starship.toml config - ansible.builtin.include_role: - name: roles/link_dotfile - vars: - link_dotfile_src: "{{ dotfiles_home }}/roles/starship/files/starship.toml" - link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.config/starship.toml" + - name: Configure Starship + tags: + - configfile + - preferences + block: + - name: Link starship.toml config + ansible.builtin.include_role: + name: roles/link_dotfile + vars: + link_dotfile_src: '{{ dotfiles_home }}/roles/starship/files/starship.toml' + link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.config/starship.toml" ``` ### Behavior **Linking Decision Tree:** + ``` 1. Check destination file status ├─ Does not exist @@ -259,8 +273,8 @@ From `roles/starship/tasks/main.yaml`: └─ Remove symlink → Create new symlink ``` -**Backup Example:** -When a non-symlink file exists at the destination, it's backed up with a timestamp: +**Backup Example:** When a non-symlink file exists at the destination, it's backed up with a timestamp: + ``` .zshrc.backup_20250219143022 ``` @@ -335,13 +349,13 @@ EOF Edit `playbooks/local_bootstrap.yml` or `playbooks/nas_bootstrap.yml`: ```yaml -- include_role: - name: roles/my_new_role - apply: - tags: my_new_role - tags: - - my_new_role - - configfile # Add to existing tag groups if appropriate + - include_role: + name: roles/my_new_role + apply: + tags: my_new_role + tags: + - my_new_role + - configfile # Add to existing tag groups if appropriate ``` #### 3. Test the Role @@ -365,50 +379,44 @@ ansible-playbook playbooks/local_bootstrap.yml \ ### Example: Role for Linking Dotfiles ```yaml -# roles/my_app/tasks/main.yml ---- -- name: Link my app configuration - ansible.builtin.include_role: - name: roles/link_dotfile - vars: - link_dotfile_src: "{{ dotfiles_home }}/roles/my_app/files/config.yaml" - link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.config/myapp/config.yaml" + - name: Link my app configuration + ansible.builtin.include_role: + name: roles/link_dotfile + vars: + link_dotfile_src: '{{ dotfiles_home }}/roles/my_app/files/config.yaml' + link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.config/myapp/config.yaml" ``` ### Example: Platform-Specific Role ```yaml -# roles/my_tool/tasks/main.yml ---- -- name: Install on macOS - when: ansible_distribution == 'MacOSX' - block: - - name: Install with Homebrew - community.general.homebrew: - name: my-tool - state: present - -- name: Install on Debian - when: ansible_distribution == 'Debian' - block: - - name: Install with apt - become: true - ansible.builtin.apt: - name: my-tool - state: present + - name: Install on macOS + when: ansible_distribution == 'MacOSX' + block: + - name: Install with Homebrew + community.general.homebrew: + name: my-tool + state: present + + - name: Install on Debian + when: ansible_distribution == 'Debian' + block: + - name: Install with apt + become: true + ansible.builtin.apt: + name: my-tool + state: present ``` ### Example: Role with External Dependencies ```yaml -# roles/my_tool/tasks/main.yml ---- -- name: Install command-line tools (macOS) - when: ansible_distribution == 'MacOSX' - include_role: - name: elliotweiser.osx-command-line-tools + - name: Install command-line tools (macOS) + when: ansible_distribution == 'MacOSX' + include_role: + name: elliotweiser.osx-command-line-tools -- name: Install the tool + - name: Install the tool # ... installation tasks ``` @@ -430,32 +438,32 @@ Variables commonly used across roles, defined in inventory (`inventory/inventory ### Path Variables ```yaml -dotfiles_home: "/path/to/dotfiles-personal" # Repository root +dotfiles_home: /path/to/dotfiles-personal # Repository root ansible_facts['user_dir']: "{{ ansible_facts['user_dir'] }}" # User home directory -homebrew_prefix: "/opt/homebrew" # Homebrew installation path (macOS) +homebrew_prefix: /opt/homebrew # Homebrew installation path (macOS) ``` ### User Variables ```yaml -ansible_user: "username" # Current user for SSH/sudo operations -admin_password: "{{ vault_pass }}" # Admin password from Ansible Vault +ansible_user: username # Current user for SSH/sudo operations +admin_password: '{{ vault_pass }}' # Admin password from Ansible Vault ``` ### Service-Specific Variables ```yaml # Docker role -docker_dir: "/path/to/docker" -caddy_dnsimple_repo_url: "git@github.com:..." -autoplex_repo_url: "git@github.com:..." -homeassistant_repo_url: "git@github.com:..." +docker_dir: /path/to/docker +caddy_dnsimple_repo_url: git@github.com:... +autoplex_repo_url: git@github.com:... +homeassistant_repo_url: git@github.com:... # Starship role -starship_version: "v1.17.1" -starship_install_directory: "/usr/local/bin" -starship_owner: "root" -starship_group: "root" +starship_version: v1.17.1 +starship_install_directory: /usr/local/bin +starship_owner: root +starship_group: root # Python role global_python_packages: @@ -469,18 +477,18 @@ pipx_packages: ### Accessing Variables In role tasks: + ```yaml -- name: Link dotfile - ansible.builtin.file: - src: "{{ dotfiles_home }}/roles/git/files/gitconfig" - dest: "{{ ansible_facts['user_dir'] }}/.gitconfig" - state: link + - name: Link dotfile + ansible.builtin.file: + src: '{{ dotfiles_home }}/roles/git/files/gitconfig' + dest: "{{ ansible_facts['user_dir'] }}/.gitconfig" + state: link ``` In role defaults: + ```yaml -# roles/my_role/defaults/main.yml ---- my_config_dir: "{{ ansible_facts['user_dir'] }}/.config/my_app" my_data_dir: "{{ ansible_facts['user_dir'] }}/.local/share/my_app" ``` @@ -492,6 +500,7 @@ my_data_dir: "{{ ansible_facts['user_dir'] }}/.local/share/my_app" The `macos` role manages three layers of macOS package installation: **1. Homebrew Formulae** (CLI tools): + ```yaml # roles/macos/defaults/main.yml brew_packages: @@ -508,6 +517,7 @@ brew_packages: ``` **2. Homebrew Casks** (GUI applications): + ```yaml brew_cask_packages: - google-chrome @@ -520,6 +530,7 @@ brew_cask_packages: ``` **3. Mac App Store Apps** (via `mas` CLI): + ```yaml mas_apps: - name: Amphetamine @@ -535,31 +546,36 @@ mas_apps: The `python` role manages Python packages through multiple installation methods: **Global pip packages**: + ```yaml global_python_packages: - package1 - package2 ``` + Installed via: `/usr/bin/pip3 install` **Isolated pipx packages**: + ```yaml pipx_packages: - package3 - package4 ``` + Installed via: `pipx install` (requires pipx from Homebrew) ### Debian Package Management The `debian` role manages system packages via apt: + ```yaml -- name: Install system packages - become: true - ansible.builtin.apt: - pkg: - - package1 - - package2 + - name: Install system packages + become: true + ansible.builtin.apt: + pkg: + - package1 + - package2 ``` ## Docker Stack Configuration @@ -572,6 +588,7 @@ roles in the repository. The 195-line `docker-compose.yml` deploys: **Media Server Stack:** + - **Plex**: Media server - **Sonarr**: TV show automation - **Radarr**: Movie automation @@ -581,6 +598,7 @@ The 195-line `docker-compose.yml` deploys: - **Autoplex/PyAutoplex**: Automated file organization **Infrastructure:** + - **PiHole**: DNS server and ad blocker - **Caddy**: Reverse proxy with automatic HTTPS - **Home Assistant**: Home automation platform @@ -589,21 +607,25 @@ The 195-line `docker-compose.yml` deploys: ### Docker Role Features **1. Docker Engine Installation:** + - Adds Docker official repository - Installs Docker CE, CLI, Containerd - Installs docker-compose plugin **2. Custom Image Builds:** + - Clones and builds custom Caddy image with DNSimple support - Builds Autoplex from private repository - Builds PyAutoplex from private repository **3. Home Assistant Setup:** + - Clones Home Assistant configuration repository - Generates `secrets.yaml` from Ansible Vault variables - Copies Lutron Caseta certificates/keys for smart home integration **4. Service Configuration:** + - Generates Caddyfile from Jinja2 template with host IP - Creates `.env` file with DNSimple OAuth token - Deploys complete docker-compose stack @@ -611,48 +633,52 @@ The 195-line `docker-compose.yml` deploys: ### Docker Role Variables Required variables in inventory: + ```yaml -docker_dir: "/path/to/docker/configs" -caddy_dnsimple_repo_url: "git@github.com:user/caddy-dnsimple.git" -caddy_dnsimple_repo_dest: "/path/to/caddy-dnsimple" -caddy_dnsimple_repo_branch: "main" -autoplex_repo_url: "git@github.com:user/autoplex.git" -autoplex_repo_dest: "/path/to/autoplex" -autoplex_repo_branch: "main" -pyautoplex_repo_url: "git@github.com:user/pyautoplex.git" -pyautoplex_repo_dest: "/path/to/pyautoplex" -pyautoplex_repo_branch: "main" -homeassistant_repo_url: "git@github.com:user/homeassistant-config.git" -homeassistant_repo_dest: "/path/to/homeassistant" -homeassistant_repo_branch: "main" +docker_dir: /path/to/docker/configs +caddy_dnsimple_repo_url: git@github.com:user/caddy-dnsimple.git +caddy_dnsimple_repo_dest: /path/to/caddy-dnsimple +caddy_dnsimple_repo_branch: main +autoplex_repo_url: git@github.com:user/autoplex.git +autoplex_repo_dest: /path/to/autoplex +autoplex_repo_branch: main +pyautoplex_repo_url: git@github.com:user/pyautoplex.git +pyautoplex_repo_dest: /path/to/pyautoplex +pyautoplex_repo_branch: main +homeassistant_repo_url: git@github.com:user/homeassistant-config.git +homeassistant_repo_dest: /path/to/homeassistant +homeassistant_repo_branch: main ``` ## Role Testing and Validation ### Pre-commit Validation -Roles are validated via pre-commit hooks (`.pre-commit-config.yaml`): +Roles are validated via prek hooks (`.pre-commit-config.yaml`): ```bash # Run all checks -pre-commit run --all-files +prek run --all-files # Or via Python script python3 run.py pre ``` **Active checks:** + - YAML validation (yamllint) - File integrity checks - Whitespace/line ending fixers **Available but commented:** + - ansible-lint (full Ansible best practices) - shellcheck (shell script validation) ### ansible-lint Configuration When enabled, ansible-lint uses `.ansible-lint.yaml`: + ```yaml # Skip certain rule categories skip_list: @@ -663,6 +689,7 @@ skip_list: ### yamllint Configuration YAML linting uses `.yamllint.yaml`: + ```yaml # Custom rules for role files rules: @@ -675,6 +702,7 @@ rules: ### Manual Testing **Dry run (check mode):** + ```bash ansible-playbook playbooks/local_bootstrap.yml \ -i inventory/inventory.yml \ @@ -684,6 +712,7 @@ ansible-playbook playbooks/local_bootstrap.yml \ ``` **Verbose output:** + ```bash ansible-playbook playbooks/local_bootstrap.yml \ -i inventory/inventory.yml \ @@ -692,6 +721,7 @@ ansible-playbook playbooks/local_bootstrap.yml \ ``` **Test specific role:** + ```bash ansible-playbook playbooks/local_bootstrap.yml \ -i inventory/inventory.yml \ @@ -721,17 +751,17 @@ ansible-galaxy collection install -r roles/requirements.yml --force Edit `roles/requirements.yml`: ```yaml ---- roles: - name: username.rolename - version: "1.0.0" # Optional: pin to specific version + version: 1.0.0 # Optional: pin to specific version collections: - name: namespace.collection - version: ">=2.0.0" # Optional: version constraint + version: '>=2.0.0' # Optional: version constraint ``` Then install: + ```bash ansible-galaxy install -r roles/requirements.yml ``` @@ -739,6 +769,7 @@ ansible-galaxy install -r roles/requirements.yml ### Role Versioning Roles in this repository follow semantic versioning through git: + - Use git tags for version tracking - Document breaking changes in role README.md - Test role changes before merging to main branch @@ -748,11 +779,13 @@ Roles in this repository follow semantic versioning through git: ### Role Not Found **Error:** + ``` ERROR! the role 'roles/my_role' was not found ``` **Solutions:** + 1. Check role name spelling in playbook 2. Verify role directory exists: `ls -la roles/my_role` 3. Ensure `tasks/main.yml` exists in role directory @@ -760,11 +793,13 @@ ERROR! the role 'roles/my_role' was not found ### External Role Not Found **Error:** + ``` ERROR! the role 'external.role' was not found ``` **Solutions:** + 1. Install dependencies: `ansible-galaxy install -r roles/requirements.yml` 2. Check `roles/requirements.yml` contains the role 3. Verify installation: `ansible-galaxy role list` @@ -772,11 +807,13 @@ ERROR! the role 'external.role' was not found ### Variable Undefined **Error:** + ``` fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable"} ``` **Solutions:** + 1. Check variable is defined in `defaults/main.yml` or `vars/main.yml` 2. Verify variable exists in inventory: `inventory/group_vars/` or `inventory/host_vars/` 3. Pass variable via command line: `-e "var_name=value"` @@ -784,11 +821,13 @@ fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an unde ### Permission Denied **Error:** + ``` fatal: [localhost]: FAILED! => {"msg": "Permission denied"} ``` **Solutions:** + 1. Add `become: true` to task requiring sudo 2. Use `--ask-become-pass` when running playbook 3. Verify sudo password is correct in vault @@ -796,11 +835,13 @@ fatal: [localhost]: FAILED! => {"msg": "Permission denied"} ### Vault Decryption Failed **Error:** + ``` ERROR! Attempting to decrypt but no vault secrets found ``` **Solutions:** + 1. Create `vault_password.txt` in repository root 2. Verify password is correct 3. Check vault file is properly encrypted: `ansible-vault view inventory/group_vars/all/vault.yml` diff --git a/roles/docker/README.md b/roles/docker/README.md index efd1b62..0b97775 100644 --- a/roles/docker/README.md +++ b/roles/docker/README.md @@ -37,7 +37,9 @@ The stack is designed to provide a fully automated media server experience with ### Media Services #### Plex (Port 32400) + **Purpose**: Media streaming server + - **Image**: `linuxserver/plex` - **Network Mode**: `host` (temporary workaround for Docker/Plex networking issues) - **Storage**: Mounts `/storage/media/tv`, `/storage/media/movies`, `/storage/media/music` @@ -45,7 +47,9 @@ The stack is designed to provide a fully automated media server experience with - **Configuration**: `/storage/media/config/plex` #### Autoplex (No exposed ports) + **Purpose**: Automated file organization for Plex + - **Image**: `danielmmetz/autoplex:latest` (custom build) - **Functionality**: Monitors Transmission download directory and copies completed files to appropriate media folders - **Mode**: Copy mode (preserves original files) @@ -57,7 +61,9 @@ The stack is designed to provide a fully automated media server experience with ### Media Lookup/Download Services #### Transmission (Port 9091) + **Purpose**: BitTorrent client + - **Image**: `linuxserver/transmission` - **Web UI**: Port 9091 - **Storage**: @@ -67,20 +73,26 @@ The stack is designed to provide a fully automated media server experience with - **Dependencies**: Used by Sonarr and Radarr for downloads #### Prowlarr (Port 9696) + **Purpose**: Torrent indexer aggregator + - **Image**: `linuxserver/prowlarr` - **Functionality**: Centralized management of torrent indexers for Sonarr and Radarr - **Configuration**: `/storage/media/config/prowlarr` - **Dependencies**: Integrates with Flaresolverr for Cloudflare bypass #### Flaresolverr (No exposed ports) + **Purpose**: Cloudflare protection bypass + - **Image**: `ghcr.io/flaresolverr/flaresolverr` - **Functionality**: Proxy service to bypass Cloudflare CAPTCHA challenges - **Dependencies**: Used by Prowlarr #### Sonarr (Port 8989) + **Purpose**: TV show management and automation + - **Image**: `linuxserver/sonarr` - **Functionality**: - Monitors TV show releases @@ -94,7 +106,9 @@ The stack is designed to provide a fully automated media server experience with - **Dependencies**: Prowlarr (indexers), Transmission (downloads) #### Radarr (Port 7878) + **Purpose**: Movie management and automation + - **Image**: `linuxserver/radarr` - **Functionality**: - Monitors movie releases @@ -110,7 +124,9 @@ The stack is designed to provide a fully automated media server experience with ### Network Services #### PiHole (Ports 53/tcp, 53/udp, 67/udp, 8053/tcp) + **Purpose**: Network-wide ad blocking and DNS server + - **Image**: `pihole/pihole:latest` - **Ports**: - 53/tcp, 53/udp: DNS service @@ -125,7 +141,9 @@ The stack is designed to provide a fully automated media server experience with ### Home Automation & Dashboards #### Home Assistant (Port 8123) + **Purpose**: Home automation platform + - **Image**: `homeassistant/home-assistant` - **Network Mode**: `host` (required for service discovery) - **Configuration**: `./homeassistant` (git repository) @@ -138,7 +156,9 @@ The stack is designed to provide a fully automated media server experience with - Shared: `./shared` #### Glance (Port 8080) + **Purpose**: Personal dashboard + - **Image**: `glanceapp/glance` - **Configuration**: `./glance/config`, `./glance/assets` - **Features**: @@ -149,14 +169,18 @@ The stack is designed to provide a fully automated media server experience with ### Disabled Services #### Caddy (Commented out) + **Purpose**: Reverse proxy with automatic HTTPS + - **Image**: `danielmmetz/caddy-dnsimple` (custom build) - **Functionality**: Would provide `*.conmason.com` subdomains for services - **Configuration**: `Caddyfile` (generated from template) - **Note**: Currently disabled in docker-compose.yml #### PyAutoplex (Commented out) + **Purpose**: Alternative Python-based file organizer + - **Image**: `connormason/pyautoplex:latest` (custom build) - **Note**: Replaced by Autoplex (Go implementation) @@ -214,21 +238,25 @@ The stack is designed to provide a fully automated media server experience with ### Data Flow 1. **Content Discovery**: + - User adds TV show to Sonarr or movie to Radarr - Sonarr/Radarr searches Prowlarr for available torrents - Prowlarr queries configured indexers (using Flaresolverr if needed) 2. **Download**: + - Sonarr/Radarr sends torrent to Transmission - Transmission downloads to `/storage/media/downloads` 3. **Organization**: + - Autoplex monitors download completion - Copies completed files to appropriate directories: - TV shows → `/storage/media/tv` - Movies → `/storage/media/movies` 4. **Consumption**: + - Plex scans media directories and makes content available - Users stream via Plex clients @@ -277,53 +305,60 @@ The stack is designed to provide a fully automated media server experience with ### Volume Mounts **Media Services:** + - Plex: Read-only access to `/storage/media/tv`, `/storage/media/movies`, `/storage/media/music` - Autoplex: Read from `/storage/media/downloads`, write to `/storage/media/tv` and `/storage/media/movies` **Download Services:** + - Transmission: Read/write to `/storage/media/downloads` and `/storage/media/watch` - Sonarr: Read-only `/storage/media/tv`, read/write `/storage/media/downloads` - Radarr: Read-only `/storage/media/movies`, read/write `/storage/media/downloads` **Configuration Persistence:** + - All services: Config directories under `/storage/media/config/*` ## Network Configuration ### Port Mapping -| Service | Port | Protocol | Purpose | -|---------|------|----------|---------| -| PiHole | 53 | TCP/UDP | DNS queries | -| PiHole | 67 | UDP | DHCP server (optional) | -| PiHole | 8053 | TCP | Web UI | -| Plex | 32400 | TCP | Media streaming (host network) | -| Transmission | 9091 | TCP | Web UI | -| Prowlarr | 9696 | TCP | Web UI | -| Sonarr | 8989 | TCP | Web UI | -| Radarr | 7878 | TCP | Web UI | -| Home Assistant | 8123 | TCP | Web UI (host network) | -| Glance | 8080 | TCP | Dashboard | +| Service | Port | Protocol | Purpose | +| -------------- | ----- | -------- | ------------------------------ | +| PiHole | 53 | TCP/UDP | DNS queries | +| PiHole | 67 | UDP | DHCP server (optional) | +| PiHole | 8053 | TCP | Web UI | +| Plex | 32400 | TCP | Media streaming (host network) | +| Transmission | 9091 | TCP | Web UI | +| Prowlarr | 9696 | TCP | Web UI | +| Sonarr | 8989 | TCP | Web UI | +| Radarr | 7878 | TCP | Web UI | +| Home Assistant | 8123 | TCP | Web UI (host network) | +| Glance | 8080 | TCP | Dashboard | ### Network Modes **Host Mode:** + - **Plex**: Uses host networking to avoid Docker networking complexities with media streaming - **Home Assistant**: Uses host networking for service discovery (mDNS, UPnP) **Bridge Mode (default):** + - All other services use Docker's default bridge network - Services communicate via container names (e.g., `sonarr:8989`) ### Reverse Proxy (Disabled) The Caddy reverse proxy configuration (currently commented out) would provide: + - HTTPS endpoints: `tv.conmason.com`, `movies.conmason.com`, `transmission.conmason.com`, `prowlarr.conmason.com`, -`plex.conmason.com` + `plex.conmason.com` - Automatic HTTPS via DNS-01 challenge with DNSimple - TLS certificate management To enable: + 1. Uncomment Caddy service in `roles/docker/files/docker-compose.yml` 2. Uncomment Caddy setup in `roles/docker/tasks/main.yml` 3. Ensure `dnsimple_oauth_token` is set in inventory vault @@ -333,30 +368,36 @@ To enable: ### Environment Variables **Common Variables (LinuxServer.io images):** + - `PUID=1000`: User ID for file permissions - `PGID=1000`: Group ID for file permissions - `VERSION=docker`: Use Docker-managed version updates - `TZ='America/Los_Angeles'`: Timezone setting **Secrets (from `.env` file):** + - `DNSIMPLE_OAUTH_TOKEN`: DNSimple API token for Caddy ACME DNS-01 - `FTLCONF_webserver_api_password`: PiHole admin password **Service-Specific:** + - PiHole: `FTLCONF_dns_listeningMode=ALL` (listen on all interfaces) - Autoplex: Command-line arguments for source/destination paths ### Configuration Files **Generated from Templates:** + - `.env`: Contains secrets from Ansible Vault - `Caddyfile`: Reverse proxy configuration with dynamic IP address - `homeassistant/secrets.yaml`: Home Assistant secrets **Static Files:** + - `docker-compose.yml`: Copied from `roles/docker/files/` **Git Repositories:** + - Home Assistant config: Cloned from `git@github.com:connormason/homeassistant.git` - Autoplex: Cloned from `https://github.com/danielmmetz/autoplex.git` - PyAutoplex: Cloned from `git@github.com:connormason/pyautoplex.git` @@ -365,6 +406,7 @@ To enable: ### Lutron Caseta Integration Home Assistant requires Lutron Caseta certificates for smart home integration: + - Certificates stored in `inventory/group_vars/all/` - Copied to `~/docker/homeassistant/` during role execution - Files: `caseta.crt`, `caseta.key`, `caseta-bridge.crt` @@ -374,6 +416,7 @@ Home Assistant requires Lutron Caseta certificates for smart home integration: ### Autoplex **Source**: `https://github.com/danielmmetz/autoplex.git` + - **Language**: Go - **Purpose**: File organization service - **Build Process**: @@ -385,6 +428,7 @@ Home Assistant requires Lutron Caseta certificates for smart home integration: ### PyAutoplex (Unused) **Source**: `git@github.com:connormason/pyautoplex.git` + - **Language**: Python - **Purpose**: Alternative file organization service - **Build Process**: Same as Autoplex @@ -393,6 +437,7 @@ Home Assistant requires Lutron Caseta certificates for smart home integration: ### Caddy DNSimple (Disabled) **Source**: `https://github.com/danielmmetz/caddy-dnsimple` + - **Purpose**: Caddy reverse proxy with DNSimple DNS integration - **Build Process**: Same as Autoplex - **Status**: Built but Caddy service commented out @@ -404,46 +449,46 @@ The role performs the following operations in order: ### 1. Docker Installation ```yaml -- Setup Docker repository (GPG key, apt repository) -- Install Docker Engine (docker-ce, docker-ce-cli, containerd.io) -- Install Docker Compose (both plugin and standalone) -- Start Docker daemon + - Setup Docker repository (GPG key, apt repository) + - Install Docker Engine (docker-ce, docker-ce-cli, containerd.io) + - Install Docker Compose (both plugin and standalone) + - Start Docker daemon ``` ### 2. Network Configuration ```yaml -- Detect host IP address (used in Caddyfile template) + - Detect host IP address (used in Caddyfile template) ``` ### 3. Custom Image Builds ```yaml -- Clone Caddy DNSimple repository -- Build caddy-dnsimple Docker image -- Generate Caddyfile from template -- Generate .env file from template + - Clone Caddy DNSimple repository + - Build caddy-dnsimple Docker image + - Generate Caddyfile from template + - Generate .env file from template -- Clone Autoplex repository -- Build autoplex Docker image + - Clone Autoplex repository + - Build autoplex Docker image -- Clone PyAutoplex repository -- Build pyautoplex Docker image + - Clone PyAutoplex repository + - Build pyautoplex Docker image ``` ### 4. Home Assistant Setup ```yaml -- Clone Home Assistant config repository -- Generate secrets.yaml from template -- Copy Lutron Caseta certificates from inventory + - Clone Home Assistant config repository + - Generate secrets.yaml from template + - Copy Lutron Caseta certificates from inventory ``` ### 5. Docker Compose Deployment ```yaml -- Copy docker-compose.yml to ~/docker/ -- Create backup of existing docker-compose.yml if present + - Copy docker-compose.yml to ~/docker/ + - Create backup of existing docker-compose.yml if present ``` **Note**: The role does NOT automatically start services (`docker-compose up`). This must be done manually. @@ -463,6 +508,7 @@ ansible-playbook playbooks/nas_bootstrap.yml -i inventory/inventory.yml --ask-be ``` This will: + 1. Install Docker 2. Build custom images 3. Deploy docker-compose.yml @@ -701,18 +747,18 @@ docker build --no-cache -t danielmmetz/autoplex:latest . Edit `roles/docker/files/docker-compose.yml`: ```yaml - newservice: - image: linuxserver/newservice - container_name: newservice - restart: unless-stopped - environment: - - PUID=1000 - - PGID=1000 - - TZ='America/Los_Angeles' - volumes: - - /storage/media/config/newservice:/config - ports: - - "8080:8080" +newservice: + image: linuxserver/newservice + container_name: newservice + restart: unless-stopped + environment: + - PUID=1000 + - PGID=1000 + - TZ='America/Los_Angeles' + volumes: + - /storage/media/config/newservice:/config + ports: + - 8080:8080 ``` ### Step 2: Add Storage Directories (if needed) @@ -753,6 +799,7 @@ docker-compose up -d newservice ### Step 5: Add to Documentation Update this README with: + - Service description in [Service Stack](#service-stack) - Port mapping in [Network Configuration](#network-configuration) - Dependencies in [Architecture](#architecture) @@ -762,36 +809,39 @@ Update this README with: If the service requires a custom image: 1. **Add repository variables** to `roles/docker/defaults/main.yml`: + ```yaml - newservice_repo_dest: "{{ docker_dir }}/newservice" + newservice_repo_dest: '{{ docker_dir }}/newservice' newservice_repo_url: https://github.com/user/newservice.git newservice_repo_branch: main ``` 2. **Add build task** to `roles/docker/tasks/main.yml`: + ```yaml - - name: Setup newservice - block: - - name: Clone newservice repo - ansible.builtin.git: - repo: "{{ newservice_repo_url }}" - dest: "{{ newservice_repo_dest }}" - version: "{{ newservice_repo_branch }}" - clone: true - update: true - accept_hostkey: true - - - name: Build newservice image - community.docker.docker_image: - build: - path: "{{ newservice_repo_dest }}" - name: user/newservice - tag: latest - source: build - become: true + - name: Setup newservice + block: + - name: Clone newservice repo + ansible.builtin.git: + repo: '{{ newservice_repo_url }}' + dest: '{{ newservice_repo_dest }}' + version: '{{ newservice_repo_branch }}' + clone: true + update: true + accept_hostkey: true + + - name: Build newservice image + community.docker.docker_image: + build: + path: '{{ newservice_repo_dest }}' + name: user/newservice + tag: latest + source: build + become: true ``` 3. **Use custom image** in docker-compose.yml: + ```yaml newservice: image: user/newservice:latest @@ -803,16 +853,19 @@ If the service requires a custom image: ### Regular Tasks **Weekly:** + - Check Docker container status: `docker-compose ps` - Review service logs for errors: `docker-compose logs --tail=100` - Monitor disk usage: `df -h /storage/media` **Monthly:** + - Update service images: `docker-compose pull && docker-compose up -d` - Clean old downloads: `rm -rf /storage/media/downloads/complete/*` - Review PiHole statistics for blocked domains **Quarterly:** + - Review and update service configurations - Check for security updates: `apt update && apt list --upgradable` - Backup Home Assistant configuration @@ -821,6 +874,7 @@ If the service requires a custom image: ### Backup Strategy **Configuration Backups:** + ```bash # Home Assistant (already in git) cd ~/docker/homeassistant @@ -837,11 +891,13 @@ tar -czf ~/backups/docker-dir-$(date +%Y%m%d).tar.gz ~/docker/ ``` **Media Library Backups:** + - Media files are large; consider separate backup strategy - Metadata can be regenerated by Plex/Sonarr/Radarr - Prioritize backing up `/storage/media/config/` over media files **Database Backups:** + ```bash # Sonarr/Radarr databases are in config directories # Stop services before backup @@ -860,6 +916,7 @@ docker-compose start sonarr radarr prowlarr ### Monitoring **Service Health:** + ```bash # All container status docker-compose ps @@ -872,6 +929,7 @@ docker-compose ps --format "table {{.Name}}\t{{.Status}}" ``` **Disk Usage:** + ```bash # Media storage du -sh /storage/media/* @@ -884,6 +942,7 @@ find /storage/media -type f -size +5G ``` **Network Monitoring:** + ```bash # Port listeners sudo netstat -tulpn | grep -E "53|32400|9091|8989|7878" @@ -911,6 +970,7 @@ Then restart Docker: `sudo systemctl restart docker` ### Security Updates **Update Docker Engine:** + ```bash sudo apt update sudo apt upgrade docker-ce docker-ce-cli containerd.io @@ -918,6 +978,7 @@ sudo systemctl restart docker ``` **Update Container Images:** + ```bash cd ~/docker docker-compose pull @@ -925,6 +986,7 @@ docker-compose up -d ``` **Audit Open Ports:** + ```bash # Check exposed ports sudo ufw status @@ -935,6 +997,7 @@ sudo ufw allow from 192.168.1.0/24 to any port 8989 # Sonarr from local network ``` **Review Credentials:** + - Rotate PiHole admin password in inventory vault - Update DNSimple OAuth token if compromised - Review Home Assistant user accounts @@ -942,6 +1005,7 @@ sudo ufw allow from 192.168.1.0/24 to any port 8989 # Sonarr from local network ### Performance Optimization **Docker Storage:** + ```bash # Clean unused images, containers, networks docker system prune -a @@ -951,11 +1015,13 @@ docker volume prune ``` **Plex Transcoding:** + - Consider hardware transcoding if available - Monitor transcoding directory size - Optimize library for direct play **Transmission:** + - Limit active downloads - Set bandwidth limits during peak hours - Clean completed downloads regularly @@ -976,6 +1042,7 @@ ansible-playbook playbooks/nas_bootstrap.yml -i inventory/inventory.yml --tags d ``` This will: + - Update docker-compose.yml - Rebuild custom images (Autoplex, Caddy) - Regenerate configuration files (.env, Caddyfile) @@ -989,26 +1056,26 @@ Note: Does not automatically restart services. Use `docker-compose up -d` after Defined in `roles/docker/defaults/main.yml`: -| Variable | Default | Description | -|----------|---------|-------------| -| `docker_dir` | `{{ ansible_facts['user_dir'] }}/docker` | Docker Compose project directory | -| `autoplex_repo_url` | `https://github.com/danielmmetz/autoplex.git` | Autoplex source repository | -| `autoplex_repo_branch` | `master` | Autoplex git branch | -| `pyautoplex_repo_url` | `git@github.com:connormason/pyautoplex.git` | PyAutoplex source repository | -| `pyautoplex_repo_branch` | `main` | PyAutoplex git branch | -| `caddy_dnsimple_repo_url` | `https://github.com/danielmmetz/caddy-dnsimple` | Caddy DNSimple source | -| `caddy_dnsimple_repo_branch` | `master` | Caddy DNSimple git branch | -| `homeassistant_repo_url` | `git@github.com:connormason/homeassistant.git` | Home Assistant config repo | -| `homeassistant_repo_branch` | `nas_bringup` | Home Assistant git branch | +| Variable | Default | Description | +| ---------------------------- | ----------------------------------------------- | -------------------------------- | +| `docker_dir` | `{{ ansible_facts['user_dir'] }}/docker` | Docker Compose project directory | +| `autoplex_repo_url` | `https://github.com/danielmmetz/autoplex.git` | Autoplex source repository | +| `autoplex_repo_branch` | `master` | Autoplex git branch | +| `pyautoplex_repo_url` | `git@github.com:connormason/pyautoplex.git` | PyAutoplex source repository | +| `pyautoplex_repo_branch` | `main` | PyAutoplex git branch | +| `caddy_dnsimple_repo_url` | `https://github.com/danielmmetz/caddy-dnsimple` | Caddy DNSimple source | +| `caddy_dnsimple_repo_branch` | `master` | Caddy DNSimple git branch | +| `homeassistant_repo_url` | `git@github.com:connormason/homeassistant.git` | Home Assistant config repo | +| `homeassistant_repo_branch` | `nas_bringup` | Home Assistant git branch | ### Required Variables from Inventory Must be defined in inventory vault files: -| Variable | Source | Used By | -|----------|--------|---------| -| `dnsimple_oauth_token` | Vault | Caddy ACME DNS-01 | -| `pihole_web_password` | Vault | PiHole web UI | +| Variable | Source | Used By | +| ---------------------- | ------ | ----------------- | +| `dnsimple_oauth_token` | Vault | Caddy ACME DNS-01 | +| `pihole_web_password` | Vault | PiHole web UI | Home Assistant secrets (defined in `templates/ha_secrets.yaml.j2`) - refer to Home Assistant repository for details. diff --git a/roles/docker/files/deploy.sh b/roles/docker/files/deploy.sh index d075925..364bcc8 100755 --- a/roles/docker/files/deploy.sh +++ b/roles/docker/files/deploy.sh @@ -53,7 +53,7 @@ main() { # Default to "all" if no arguments if [[ ${#services[@]} -eq 0 ]] || [[ "${services[0]}" == "all" ]]; then - read -ra services <<< "$(discover_services)" + read -ra services <<<"$(discover_services)" echo "Deploying all services: ${services[*]}" fi diff --git a/roles/link_dotfile/README.md b/roles/link_dotfile/README.md index 49d1098..84de5ee 100644 --- a/roles/link_dotfile/README.md +++ b/roles/link_dotfile/README.md @@ -2,21 +2,24 @@ ## Purpose -A reusable utility role for safely creating symbolic links from dotfiles in the repository to their target locations -in the user's home directory. This role handles validation, backup, and idempotent linking operations. +A reusable utility role for safely creating symbolic links from dotfiles in the repository to their target locations in +the user's home directory. This role handles validation, backup, and idempotent linking operations. ## What It Does 1. **Validates inputs**: + - Ensures required variables are defined - Verifies source file exists before linking 2. **Handles existing files**: + - Backs up existing non-symlink files with timestamp - Removes incorrect symlinks - Preserves correct existing symlinks (idempotent) 3. **Creates symlinks**: + - Creates parent directories as needed - Links source to destination - Reports when files are already correctly linked @@ -25,15 +28,15 @@ in the user's home directory. This role handles validation, backup, and idempote This role must be called with two required variables: -| Variable | Description | Example | -|----------|-------------|---------| -| `link_dotfile_src` | Absolute path to source file in dotfiles repo | `{{ dotfiles_home }}/roles/git/files/gitconfig` | -| `link_dotfile_dst` | Absolute path to destination (target location) | `{{ ansible_facts['user_dir'] }}/.gitconfig` | +| Variable | Description | Example | +| ------------------ | ---------------------------------------------- | ----------------------------------------------- | +| `link_dotfile_src` | Absolute path to source file in dotfiles repo | `{{ dotfiles_home }}/roles/git/files/gitconfig` | +| `link_dotfile_dst` | Absolute path to destination (target location) | `{{ ansible_facts['user_dir'] }}/.gitconfig` | ## Optional Variables -| Variable | Description | Default | -|----------|-------------|---------| +| Variable | Description | Default | +| ------------------ | ------------------------------------------ | --------------------- | | `dotfile_dir_mode` | Permissions for created parent directories | `omit` (uses default) | ## Usage Examples @@ -43,39 +46,39 @@ This role must be called with two required variables: Include the role with required variables: ```yaml -- name: Link git config - ansible.builtin.include_role: - name: roles/link_dotfile - vars: - link_dotfile_src: "{{ dotfiles_home }}/roles/git/files/gitconfig" - link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.gitconfig" + - name: Link git config + ansible.builtin.include_role: + name: roles/link_dotfile + vars: + link_dotfile_src: '{{ dotfiles_home }}/roles/git/files/gitconfig' + link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.gitconfig" ``` ### Link Multiple Files in a Loop ```yaml -- name: Link shell dotfiles - ansible.builtin.include_role: - name: roles/link_dotfile - vars: - link_dotfile_src: "{{ dotfiles_home }}/roles/shell/files/{{ item }}" - link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.{{ item }}" - loop: - - zshrc - - zprofile - - zshenv + - name: Link shell dotfiles + ansible.builtin.include_role: + name: roles/link_dotfile + vars: + link_dotfile_src: '{{ dotfiles_home }}/roles/shell/files/{{ item }}' + link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.{{ item }}" + loop: + - zshrc + - zprofile + - zshenv ``` ### Specify Directory Permissions ```yaml -- name: Link config with custom directory mode - ansible.builtin.include_role: - name: roles/link_dotfile - vars: - link_dotfile_src: "{{ dotfiles_home }}/roles/app/files/config.yml" - link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.config/app/config.yml" - dotfile_dir_mode: "0700" + - name: Link config with custom directory mode + ansible.builtin.include_role: + name: roles/link_dotfile + vars: + link_dotfile_src: '{{ dotfiles_home }}/roles/app/files/config.yml' + link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.config/app/config.yml" + dotfile_dir_mode: '0700' ``` ## Behavior Details @@ -181,17 +184,17 @@ link_dotfile_dst: /Users/username/.config/app/config.yml ```yaml # Good -- include_role: - name: roles/link_dotfile - vars: - link_dotfile_src: "{{ dotfiles_home }}/files/{{ item }}" - link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.{{ item }}" - loop: [file1, file2, file3] + - include_role: + name: roles/link_dotfile + vars: + link_dotfile_src: '{{ dotfiles_home }}/files/{{ item }}' + link_dotfile_dst: "{{ ansible_facts['user_dir'] }}/.{{ item }}" + loop: [file1, file2, file3] # Bad (repetitive) -- include_role: ... # file1 -- include_role: ... # file2 -- include_role: ... # file3 + - include_role: '...' # file1 + - include_role: '...' # file2 + - include_role: '...' # file3 ``` ## Testing diff --git a/roles/macos/README.md b/roles/macos/README.md index 16a60a9..bf6bd23 100644 --- a/roles/macos/README.md +++ b/roles/macos/README.md @@ -23,11 +23,13 @@ environment and quickly bootstrap new macOS systems. **Purpose**: Command-line tools and utilities **When to use**: + - CLI tools and utilities (e.g., `git`, `jq`, `ripgrep`) - Development tools without GUIs (e.g., `node`, `python`, `hatch`) - System utilities (e.g., `direnv`, `autojump`) **Example packages**: + ```yaml brew_packages: - bat # cat alternative with syntax highlighting @@ -37,6 +39,7 @@ brew_packages: ``` **Special syntax**: + - Standard packages: `package-name` - Tap packages: `username/tap-name/package-name` (e.g., `jesseduffield/lazydocker/lazydocker`) @@ -45,12 +48,14 @@ brew_packages: **Purpose**: GUI applications distributed outside the Mac App Store **When to use**: + - Desktop applications with graphical interfaces - Developer tools (IDEs, editors) - Third-party applications not available in the Mac App Store - Applications requiring more frequent updates than App Store allows **Example packages**: + ```yaml brew_cask_packages: - google-chrome # Web browser @@ -60,6 +65,7 @@ brew_cask_packages: ``` **Benefits**: + - No App Store sandboxing restrictions - Often more up-to-date than App Store versions - Can install beta/nightly builds @@ -70,12 +76,14 @@ brew_cask_packages: **Purpose**: Applications distributed through Apple's Mac App Store **When to use**: + - Applications only available in the Mac App Store - Applications requiring App Store entitlements - Apple's own applications - Apps where App Store version is preferred **Example packages**: + ```yaml mas_apps: - name: Amphetamine # Keep Mac awake utility @@ -85,10 +93,12 @@ mas_apps: ``` **Requirements**: + - Must be signed into Mac App Store with Apple ID - App ID can be found in the App Store URL (e.g., `https://apps.apple.com/app/id937984704`) **Finding App IDs**: + ```bash # Search for an app mas search "app name" @@ -102,7 +112,6 @@ mas list All package lists are defined in `roles/macos/defaults/main.yml`: ```yaml ---- brew_packages: - package1 - package2 @@ -121,6 +130,7 @@ mas_upgrade_all_apps: false ### Current Package Inventory **CLI Tools** (29 packages): + - Version control: `git-delta`, `git-extras`, `git-lfs`, `gh`, `hub`, `lazygit` - Development: `hatch`, `pipx`, `node`, `mermaid-cli` - File utilities: `bat`, `ripgrep`, `tree`, `jq` @@ -130,6 +140,7 @@ mas_upgrade_all_apps: false - System tools: `shellcheck`, `nmap`, `mas`, `just`, `moor` **GUI Applications** (8 packages): + - Browsers: `google-chrome` - Development: `pycharm-ce`, `sublime-text`, `macdown`, `iterm2` - Automation: `hammerspoon` @@ -137,6 +148,7 @@ mas_upgrade_all_apps: false - Media: `spotify` **Mac App Store** (3 apps): + - System monitoring: `iStat Menus` - Utilities: `Amphetamine` - VPN: `Tailscale` @@ -189,6 +201,7 @@ brew_cask_packages: ### Adding Mac App Store Apps 1. Find the app ID from the Mac App Store: + - Visit app page in browser - Copy ID from URL: `https://apps.apple.com/app/id123456789` - Or use `mas search "app name"` @@ -257,12 +270,12 @@ ansible-galaxy install -r roles/requirements.yml All variables defined in `roles/macos/defaults/main.yml`: -| Variable | Type | Description | Default | -|----------|------|-------------|---------| -| `brew_packages` | list[string] | Homebrew formulae to install | See defaults file | -| `brew_cask_packages` | list[string] | Homebrew casks to install | See defaults file | -| `mas_apps` | list[object] | Mac App Store apps to install | See defaults file | -| `mas_upgrade_all_apps` | boolean | Whether to upgrade all installed MAS apps | `false` | +| Variable | Type | Description | Default | +| ---------------------- | ------------ | ----------------------------------------- | ----------------- | +| `brew_packages` | list[string] | Homebrew formulae to install | See defaults file | +| `brew_cask_packages` | list[string] | Homebrew casks to install | See defaults file | +| `mas_apps` | list[object] | Mac App Store apps to install | See defaults file | +| `mas_upgrade_all_apps` | boolean | Whether to upgrade all installed MAS apps | `false` | ### Variable Overrides @@ -381,8 +394,8 @@ ansible-playbook playbooks/local_bootstrap.yml \ ``` Example `custom_packages.yml`: + ```yaml ---- brew_packages: - git - vim @@ -402,6 +415,7 @@ mas_apps: [] **Cause**: The `mas` CLI tool is installed via `brew_packages` but may not be in PATH yet during the same playbook run. **Solution**: Run the playbook twice, or install `mas` manually first: + ```bash brew install mas ``` @@ -411,6 +425,7 @@ brew install mas **Cause**: Not signed into Mac App Store with Apple ID. **Solution**: + 1. Open Mac App Store 2. Sign in with your Apple ID 3. Re-run the playbook @@ -420,10 +435,12 @@ brew install mas **Cause**: Application was installed manually, conflicts with Homebrew-managed version. **Solution**: Remove manual installation first: + ```bash brew uninstall --cask app-name --force rm -rf /Applications/AppName.app ``` + Then re-run playbook. #### Issue: Command-line tools installation hangs @@ -431,6 +448,7 @@ Then re-run playbook. **Cause**: Interactive prompts waiting for user input. **Solution**: Install manually first: + ```bash xcode-select --install ``` @@ -440,6 +458,7 @@ xcode-select --install **Cause**: Homebrew directories have incorrect ownership. **Solution**: Fix Homebrew permissions: + ```bash sudo chown -R $(whoami) $(brew --prefix)/* ``` @@ -449,6 +468,7 @@ sudo chown -R $(whoami) $(brew --prefix)/* **Cause**: Network issues or slow connection. **Solution**: Retry the playbook, or install individual package manually: + ```bash brew install package-name brew install --cask app-name @@ -458,11 +478,13 @@ mas install app-id ### Debugging Tips 1. **Check Homebrew status**: + ```bash brew doctor ``` 2. **List installed packages**: + ```bash brew list # formulae brew list --cask # casks @@ -470,6 +492,7 @@ mas list # Mac App Store apps ``` 3. **Check available updates**: + ```bash brew outdated brew outdated --cask @@ -477,11 +500,13 @@ mas outdated ``` 4. **View Homebrew logs**: + ```bash brew install --verbose package-name ``` 5. **Test playbook with increased verbosity**: + ```bash ansible-playbook playbooks/local_bootstrap.yml \ -i inventory/inventory.yml \ @@ -509,6 +534,7 @@ ansible-playbook playbooks/local_bootstrap.yml \ ### Keeping Package Lists Updated 1. **Review installed packages**: + ```bash brew list | wc -l # count formulae brew list --cask | wc -l # count casks @@ -517,11 +543,13 @@ brew list --cask | wc -l # count casks 2. **Remove unused packages** from `defaults/main.yml` 3. **Update Homebrew itself**: + ```bash brew update ``` 4. **Upgrade all packages** (outside Ansible): + ```bash brew upgrade brew upgrade --cask @@ -546,7 +574,8 @@ When modifying this role: 2. Add comments for non-obvious package choices 3. Test changes on a clean macOS system if possible 4. Update this README if adding new variables or functionality -5. Run pre-commit hooks before committing: +5. Run prek hooks before committing: + ```bash -pre-commit run --all-files +prek run --all-files ``` diff --git a/roles/starship/files/starship.toml b/roles/starship/files/starship.toml index d4b2ea1..480ea17 100644 --- a/roles/starship/files/starship.toml +++ b/roles/starship/files/starship.toml @@ -430,61 +430,61 @@ charging_symbol = "󰂉" discharging_symbol = "󰂎" unknown_symbol = "󰂑" -[[battery.display]] # Battery/charging display settings when capacity between 90% and 100% +[[battery.display]] # Battery/charging display settings when capacity between 90% and 100% threshold = 100 style = "bright-green" discharging_symbol = "󰁹" charging_symbol = "󰂅" -[[battery.display]] # Battery/charging display settings when capacity between 80% and 90% +[[battery.display]] # Battery/charging display settings when capacity between 80% and 90% threshold = 90 style = "green" discharging_symbol = "󰂂" charging_symbol = "󰂋" -[[battery.display]] # Battery/charging display settings when capacity between 70% and 80% +[[battery.display]] # Battery/charging display settings when capacity between 70% and 80% threshold = 80 style = "green" discharging_symbol = "󰂁" charging_symbol = "󰂊" -[[battery.display]] # Battery/charging display settings when capacity between 60% and 70% +[[battery.display]] # Battery/charging display settings when capacity between 60% and 70% threshold = 70 style = "green" discharging_symbol = "󰂀" charging_symbol = "󰢞" -[[battery.display]] # Battery/charging display settings when capacity between 50% and 60% +[[battery.display]] # Battery/charging display settings when capacity between 50% and 60% threshold = 60 style = "green" discharging_symbol = "󰁿" charging_symbol = "󰂉" -[[battery.display]] # Battery/charging display settings when capacity between 40% and 50% +[[battery.display]] # Battery/charging display settings when capacity between 40% and 50% threshold = 50 style = "green" discharging_symbol = "󰁾" charging_symbol = "󰢝" -[[battery.display]] # Battery/charging display settings when capacity between 30% and 40% +[[battery.display]] # Battery/charging display settings when capacity between 30% and 40% threshold = 40 style = "dimmed bright-yellow" discharging_symbol = "󰁽" charging_symbol = "󰂈" -[[battery.display]] # Battery/charging display settings when capacity between 20% and 30% +[[battery.display]] # Battery/charging display settings when capacity between 20% and 30% threshold = 30 style = "orange" discharging_symbol = "󰁼" charging_symbol = "󰂇" -[[battery.display]] # Battery/charging display settings when capacity between 10% and 20% +[[battery.display]] # Battery/charging display settings when capacity between 10% and 20% threshold = 20 style = "bright-red" discharging_symbol = "󰁻" charging_symbol = "󰂆" -[[battery.display]] # Battery/charging display settings when capacity between 80% and 10% +[[battery.display]] # Battery/charging display settings when capacity between 80% and 10% threshold = 10 style = "red" discharging_symbol = "󰁺" diff --git a/roles/tailscale/README.md b/roles/tailscale/README.md index b812e8b..4fcb38d 100644 --- a/roles/tailscale/README.md +++ b/roles/tailscale/README.md @@ -24,8 +24,8 @@ Sets up [Tailscale VPN](https://tailscale.com/) on Debian and macOS systems. Set these in your Ansible Vault (`inventory/group_vars/all/vault.yml`): ```yaml -tailscale_auth_key: "tskey-auth-..." -tailscale_tailnet_name: "your-tailnet.ts.net" +tailscale_auth_key: tskey-auth-... +tailscale_tailnet_name: your-tailnet.ts.net ``` ## Optional Variables @@ -37,28 +37,28 @@ Key overrides for NAS (`inventory/host_vars/nas/vars.yml`): ```yaml tailscale_https_enabled: true tailscale_services: - - { name: sonarr, port: 8989, path: sonarr } - - { name: radarr, port: 7878, path: radarr } - - { name: transmission, port: 9091, path: transmission } - - { name: prowlarr, port: 9696, path: prowlarr } - - { name: pihole, port: 80, path: pihole } - - { name: glance, port: 8080, path: glance, strip_prefix: true } - - { name: plex, port: 32400, dedicated_port: 32443 } + - {name: sonarr, port: 8989, path: sonarr} + - {name: radarr, port: 7878, path: radarr} + - {name: transmission, port: 9091, path: transmission} + - {name: prowlarr, port: 9696, path: prowlarr} + - {name: pihole, port: 80, path: pihole} + - {name: glance, port: 8080, path: glance, strip_prefix: true} + - {name: plex, port: 32400, dedicated_port: 32443} ``` ## Service Routing Services are accessible via `https://nas..ts.net/`: -| Service | URL | Notes | -|---|---|---| -| Sonarr | `/sonarr` | Requires `UrlBase = /sonarr` in app settings | -| Radarr | `/radarr` | Requires `UrlBase = /radarr` in app settings | -| Prowlarr | `/prowlarr` | Requires `UrlBase = /prowlarr` in app settings | -| Transmission | `/transmission` | Requires `rpc-url = /transmission/` in settings.json | -| PiHole | `/pihole` | Automatically rewrites to `/admin` | -| Glance | `/glance` | No app config needed | -| Plex | `:32443` (dedicated port) | Access via `https://nas..ts.net:32443` | +| Service | URL | Notes | +| ------------ | ------------------------- | ---------------------------------------------------- | +| Sonarr | `/sonarr` | Requires `UrlBase = /sonarr` in app settings | +| Radarr | `/radarr` | Requires `UrlBase = /radarr` in app settings | +| Prowlarr | `/prowlarr` | Requires `UrlBase = /prowlarr` in app settings | +| Transmission | `/transmission` | Requires `rpc-url = /transmission/` in settings.json | +| PiHole | `/pihole` | Automatically rewrites to `/admin` | +| Glance | `/glance` | No app config needed | +| Plex | `:32443` (dedicated port) | Access via `https://nas..ts.net:32443` | ## Post-Deployment Steps diff --git a/roles/zsh/README.md b/roles/zsh/README.md index fb03961..62b8309 100644 --- a/roles/zsh/README.md +++ b/roles/zsh/README.md @@ -3,6 +3,7 @@ Sets up zsh shell ## Tasks + - Install oh-my-zsh - Set zsh as default shell - Install [powerlevel10k zsh theme](https://github.com/romkatv/powerlevel10k.git) and copy theme config diff --git a/roles/zsh/files/p10k.zsh b/roles/zsh/files/p10k.zsh index 077b11d..6f7a989 100644 --- a/roles/zsh/files/p10k.zsh +++ b/roles/zsh/files/p10k.zsh @@ -13,1362 +13,1362 @@ # Temporarily change options. 'builtin' 'local' '-a' 'p10k_config_opts' -[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases') -[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob') +[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases') +[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob') [[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand') 'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand' () { - emulate -L zsh - setopt no_unset extended_glob - - # Unset all configuration options. This allows you to apply configiguration changes without - # restarting zsh. Edit ~/.p10k.zsh and type `source ~/.p10k.zsh`. - unset -m 'POWERLEVEL9K_*' - - autoload -Uz is-at-least && is-at-least 5.1 || return - - zmodload zsh/langinfo - if [[ ${langinfo[CODESET]:-} != (utf|UTF)(-|)8 ]]; then - local LC_ALL=${${(@M)$(locale -a):#*.(utf|UTF)(-|)8}[1]:-en_US.UTF-8} - fi - - # The list of segments shown on the left. Fill it with the most important segments. - typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=( - # =========================[ Line #1 ]========================= - # os_icon # os identifier - dir # current directory - vcs # git status - # =========================[ Line #2 ]========================= - newline # \n - # prompt_char # prompt symbol - ) - - # The list of segments shown on the right. Fill it with less important segments. - # Right prompt on the last prompt line (where you are typing your commands) gets - # automatically hidden when the input line reaches it. Right prompt above the - # last prompt line gets hidden if it would overlap with left prompt. - typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( - # =========================[ Line #1 ]========================= - status # exit code of the last command - command_execution_time # duration of the last command - background_jobs # presence of background jobs - direnv # direnv status (https://direnv.net/) - asdf # asdf version manager (https://github.com/asdf-vm/asdf) - virtualenv # python virtual environment (https://docs.python.org/3/library/venv.html) - anaconda # conda environment (https://conda.io/) - pyenv # python environment (https://github.com/pyenv/pyenv) - goenv # go environment (https://github.com/syndbg/goenv) - nodenv # node.js version from nodenv (https://github.com/nodenv/nodenv) - nvm # node.js version from nvm (https://github.com/nvm-sh/nvm) - nodeenv # node.js environment (https://github.com/ekalinin/nodeenv) - # node_version # node.js version - # go_version # go version (https://golang.org) - # rust_version # rustc version (https://www.rust-lang.org) - # dotnet_version # .NET version (https://dotnet.microsoft.com) - rbenv # ruby version from rbenv (https://github.com/rbenv/rbenv) - rvm # ruby version from rvm (https://rvm.io) - fvm # flutter version management (https://github.com/leoafarias/fvm) - luaenv # lua version from luaenv (https://github.com/cehoffman/luaenv) - jenv # java version from jenv (https://github.com/jenv/jenv) - plenv # perl version from plenv (https://github.com/tokuhirom/plenv) - kubecontext # current kubernetes context (https://kubernetes.io/) - terraform # terraform workspace (https://www.terraform.io) - aws # aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) - aws_eb_env # aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) - azure # azure account name (https://docs.microsoft.com/en-us/cli/azure) - gcloud # google cloud cli account and project (https://cloud.google.com/) - google_app_cred # google application credentials (https://cloud.google.com/docs/authentication/production) - context # user@hostname - nordvpn # nordvpn connection status, linux only (https://nordvpn.com/) - ranger # ranger shell (https://github.com/ranger/ranger) - nnn # nnn shell (https://github.com/jarun/nnn) - vim_shell # vim shell indicator (:sh) - midnight_commander # midnight commander shell (https://midnight-commander.org/) - nix_shell # nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) - vi_mode # vi mode (you don't need this if you've enabled prompt_char) - # load # CPU load - # disk_usage # disk usage - # ram # free RAM - # swap # used swap - todo # todo items (https://github.com/todotxt/todo.txt-cli) - timewarrior # timewarrior tracking status (https://timewarrior.net/) - time # current time - # =========================[ Line #2 ]========================= - newline # \n - # ip # ip address and bandwidth usage for a specified network interface - # public_ip # public IP address - # proxy # system-wide http/https/ftp proxy - # battery # internal battery - # wifi # wifi speed - # example # example user-defined segment (see prompt_example function below) - ) - - # To enable default icons for all segments, don't define POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION - # or set it to '${P9K_VISUAL_IDENTIFIER}'. - # - # To remove trailing space from all default icons, set POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION - # to '${P9K_VISUAL_IDENTIFIER% }'. - # - # To enable default icons for one segment (e.g., dir), set - # POWERLEVEL9K_DIR_VISUAL_IDENTIFIER_EXPANSION='${P9K_VISUAL_IDENTIFIER}'. - # - # To assign a specific icon to one segment (e.g., dir), set - # POWERLEVEL9K_DIR_VISUAL_IDENTIFIER_EXPANSION='⭐'. - # - # To assign a specific icon to a segment in a given state (e.g., dir in state NOT_WRITABLE), - # set POWERLEVEL9K_DIR_NOT_WRITABLE_VISUAL_IDENTIFIER_EXPANSION='⭐'. - # - # Note: You can use $'\u2B50' instead of '⭐'. It's especially convenient when specifying - # icons that your text editor cannot render. Don't forget to put $ and use single quotes when - # defining icons via Unicode codepoints. - # - # Note: Many default icons cannot be displayed with system fonts. You'll need to install a - # capable font to use them. See POWERLEVEL9K_MODE below. - typeset -g POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION='${P9K_VISUAL_IDENTIFIER}' - - # This option makes a difference only when default icons are enabled for all or some prompt - # segments (see POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION above). LOCK_ICON can be printed as - # $'\uE0A2', $'\uE138' or $'\uF023' depending on POWERLEVEL9K_MODE. The correct value of this - # parameter depends on the provider of the font your terminal is using. - # - # Font Provider | POWERLEVEL9K_MODE - # ---------------------------------+------------------- - # Powerline | powerline - # Font Awesome | awesome-fontconfig - # Adobe Source Code Pro | awesome-fontconfig - # Source Code Pro | awesome-fontconfig - # Awesome-Terminal Fonts (regular) | awesome-fontconfig - # Awesome-Terminal Fonts (patched) | awesome-patched - # Nerd Fonts | nerdfont-complete - # Other | compatible - # - # If this looks overwhelming, either stick with a preinstalled system font and set - # POWERLEVEL9K_MODE=compatible, or install the recommended Powerlevel10k font from - # https://github.com/romkatv/powerlevel10k/#recommended-meslo-nerd-font-patched-for-powerlevel10k - # and set POWERLEVEL9K_MODE=nerdfont-complete. - typeset -g POWERLEVEL9K_MODE=compatible - - # When set to true, icons appear before content on both sides of the prompt. When set - # to false, icons go after content. If empty or not set, icons go before content in the left - # prompt and after content in the right prompt. - # - # You can also override it for a specific segment: - # - # POWERLEVEL9K_STATUS_ICON_BEFORE_CONTENT=false - # - # Or for a specific segment in specific state: - # - # POWERLEVEL9K_DIR_NOT_WRITABLE_ICON_BEFORE_CONTENT=false - typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT= - - # Add an empty line before each prompt. - typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=true - - # Connect left prompt lines with these symbols. You'll probably want to use the same color - # as POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND below. - typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_PREFIX='%242F╭─' - typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_PREFIX='%242F├─' - typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_PREFIX='%242F╰─' - # Connect right prompt lines with these symbols. - typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_SUFFIX='%242F─╮' - typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_SUFFIX='%242F─┤' - typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_SUFFIX='%242F─╯' - - # Filler between left and right prompt on the first prompt line. You can set it to ' ', '·' or - # '─'. The last two make it easier to see the alignment between left and right prompt and to - # separate prompt from command output. You might want to set POWERLEVEL9K_PROMPT_ADD_NEWLINE=false - # for more compact prompt if using using this option. - typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR=' ' - typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_BACKGROUND= - if [[ $POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR != ' ' ]]; then - # The color of the filler. You'll probably want to match the color of POWERLEVEL9K_MULTILINE - # ornaments defined above. - typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND=242 - # Start filler from the edge of the screen if there are no left segments on the first line. - typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_FIRST_SEGMENT_END_SYMBOL='%{%}' - # End filler on the edge of the screen if there are no right segments on the first line. - typeset -g POWERLEVEL9K_EMPTY_LINE_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='%{%}' - fi - - # Default background color. - typeset -g POWERLEVEL9K_BACKGROUND=238 - - # Separator between same-color segments on the left. - typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR='%246F|' - # Separator between same-color segments on the right. - typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR='%246F|' - # Separator between different-color segments on the left. - typeset -g POWERLEVEL9K_LEFT_SEGMENT_SEPARATOR='' - # Separator between different-color segments on the right. - typeset -g POWERLEVEL9K_RIGHT_SEGMENT_SEPARATOR='' - # The right end of left prompt. - typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='▓▒░' - # The left end of right prompt. - typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='░▒▓' - # The left end of left prompt. - typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL='' - # The right end of right prompt. - typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL='' - # Left prompt terminator for lines without any segments. - typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= - - #################################[ os_icon: os identifier ]################################## - # OS identifier color. - typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND=255 - # Make the icon bold. - typeset -g POWERLEVEL9K_OS_ICON_CONTENT_EXPANSION='%B${P9K_CONTENT}' - - ################################[ prompt_char: prompt symbol ]################################ - # Transparent background. - typeset -g POWERLEVEL9K_PROMPT_CHAR_BACKGROUND= - # Green prompt symbol if the last command succeeded. - typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=76 - # Red prompt symbol if the last command failed. - typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=196 - # Default prompt symbol. - typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION='❯' - # Prompt symbol in command vi mode. - typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION='❮' - # Prompt symbol in visual vi mode. - typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='Ⅴ' - # Prompt symbol in overwrite vi mode. - typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION='▶' - typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true - # No line terminator if prompt_char is the last segment. - typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= - # No line introducer if prompt_char is the first segment. - typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL= - # No surrounding whitespace. - typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_{LEFT,RIGHT}_WHITESPACE= - - ##################################[ dir: current directory ]################################## - # Default current directory color. - typeset -g POWERLEVEL9K_DIR_FOREGROUND=31 - # If directory is too long, shorten some of its segments to the shortest possible unique - # prefix. The shortened directory can be tab-completed to the original. - typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique - # Replace removed segment suffixes with this symbol. - typeset -g POWERLEVEL9K_SHORTEN_DELIMITER= - # Color of the shortened directory segments. - typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=103 - # Color of the anchor directory segments. Anchor segments are never shortened. The first - # segment is always an anchor. - typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=39 - # Display anchor directory segments in bold. - typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=true - # Don't shorten directories that contain any of these files. They are anchors. - local anchor_files=( - .bzr - .citc - .git - .hg - .node-version - .python-version - .ruby-version - .shorten_folder_marker - .svn - .terraform - CVS - Cargo.toml - composer.json - go.mod - package.json - ) - typeset -g POWERLEVEL9K_SHORTEN_FOLDER_MARKER="(${(j:|:)anchor_files})" - # If set to true, remove everything before the last (deepest) subdirectory that contains files - # matching $POWERLEVEL9K_SHORTEN_FOLDER_MARKER. For example, when the current directory is - # /foo/bar/git_repo/baz, prompt will display git_repo/baz. This assumes that /foo/bar/git_repo - # contains a marker (.git) and other directories don't. - typeset -g POWERLEVEL9K_DIR_TRUNCATE_BEFORE_MARKER=false - # Don't shorten this many last directory segments. They are anchors. - typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=1 - # Shorten directory if it's longer than this even if there is space for it. The value can - # be either absolute (e.g., '80') or a percentage of terminal width (e.g, '50%'). If empty, - # directory will be shortened only when prompt doesn't fit or when other parameters demand it - # (see POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS and POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT below). - # If set to `0`, directory will always be shortened to its minimum length. - typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80 - # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least this - # many columns for typing commands. - typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40 - # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least - # COLUMNS * POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT * 0.01 columns for typing commands. - typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT=50 - # If set to true, embed a hyperlink into the directory. Useful for quickly - # opening a directory in the file manager simply by clicking the link. - # Can also be handy when the directory is shortened, as it allows you to see - # the full directory that was used in previous commands. - typeset -g POWERLEVEL9K_DIR_HYPERLINK=false - - # Enable special styling for non-writable directories. - typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=true - # Show this icon when the current directory is not writable. POWERLEVEL9K_DIR_SHOW_WRITABLE - # above must be set to true for this parameter to have effect. - typeset -g POWERLEVEL9K_DIR_NOT_WRITABLE_VISUAL_IDENTIFIER_EXPANSION='∅' - - # Custom prefix. - # typeset -g POWERLEVEL9K_DIR_PREFIX='%248Fin ' - - # POWERLEVEL9K_DIR_CLASSES allows you to specify custom icons for different directories. - # It must be an array with 3 * N elements. Each triplet consists of: - # - # 1. A pattern against which the current directory is matched. Matching is done with - # extended_glob option enabled. - # 2. Directory class for the purpose of styling. - # 3. Icon. - # - # Triplets are tried in order. The first triplet whose pattern matches $PWD wins. If there - # are no matches, the directory will have no icon. - # - # Example: - # - # typeset -g POWERLEVEL9K_DIR_CLASSES=( - # '~/work(|/*)' WORK '(╯°□°)╯︵ ┻━┻' - # '~(|/*)' HOME '⌂' - # '*' DEFAULT '') - # - # With these settings, the current directory in the prompt may look like this: - # - # (╯°□°)╯︵ ┻━┻ ~/work/projects/important/urgent - # - # Or like this: - # - # ⌂ ~/best/powerlevel10k - # - # You can also set different colors for directories of different classes. Remember to override - # FOREGROUND, SHORTENED_FOREGROUND and ANCHOR_FOREGROUND for every directory class that you wish - # to have its own color. - # - # typeset -g POWERLEVEL9K_DIR_WORK_FOREGROUND=31 - # typeset -g POWERLEVEL9K_DIR_WORK_SHORTENED_FOREGROUND=103 - # typeset -g POWERLEVEL9K_DIR_WORK_ANCHOR_FOREGROUND=39 - # - typeset -g POWERLEVEL9K_DIR_CLASSES=() - - #####################################[ vcs: git status ]###################################### - # Branch icon. Set this parameter to '\uF126 ' for the popular Powerline branch icon. - typeset -g POWERLEVEL9K_VCS_BRANCH_ICON= - POWERLEVEL9K_VCS_BRANCH_ICON=${(g::)POWERLEVEL9K_VCS_BRANCH_ICON} - - # Untracked files icon. It's really a question mark, your font isn't broken. - # Change the value of this parameter to show a different icon. - typeset -g POWERLEVEL9K_VCS_UNTRACKED_ICON='?' - POWERLEVEL9K_VCS_UNTRACKED_ICON=${(g::)POWERLEVEL9K_VCS_UNTRACKED_ICON} - - # Formatter for Git status. - # - # Example output: master ⇣42⇡42 *42 merge ~42 +42 !42 ?42. - # - # You can edit the function to customize how Git status looks. - # - # VCS_STATUS_* parameters are set by gitstatus plugin. See reference: - # https://github.com/romkatv/gitstatus/blob/master/gitstatus.plugin.zsh. - function my_git_formatter() { emulate -L zsh + setopt no_unset extended_glob - if [[ -n $P9K_CONTENT ]]; then - # If P9K_CONTENT is not empty, use it. It's either "loading" or from vcs_info (not from - # gitstatus plugin). VCS_STATUS_* parameters are not available in this case. - typeset -g my_git_format=$P9K_CONTENT - return - fi + # Unset all configuration options. This allows you to apply configiguration changes without + # restarting zsh. Edit ~/.p10k.zsh and type `source ~/.p10k.zsh`. + unset -m 'POWERLEVEL9K_*' - if (( $1 )); then - # Styling for up-to-date Git status. - local meta='%248F' # grey foreground - local clean='%76F' # green foreground - local modified='%178F' # yellow foreground - local untracked='%39F' # blue foreground - local conflicted='%196F' # red foreground - else - # Styling for incomplete and stale Git status. - local meta='%244F' # grey foreground - local clean='%244F' # grey foreground - local modified='%244F' # grey foreground - local untracked='%244F' # grey foreground - local conflicted='%244F' # grey foreground - fi + autoload -Uz is-at-least && is-at-least 5.1 || return - local res - local where # branch or tag - if [[ -n $VCS_STATUS_LOCAL_BRANCH ]]; then - res+="${clean}${POWERLEVEL9K_VCS_BRANCH_ICON}" - where=${(V)VCS_STATUS_LOCAL_BRANCH} - elif [[ -n $VCS_STATUS_TAG ]]; then - res+="${meta}#" - where=${(V)VCS_STATUS_TAG} + zmodload zsh/langinfo + if [[ ${langinfo[CODESET]:-} != (utf|UTF)(-|)8 ]]; then + local LC_ALL=${${(@M)$(locale -a):#*.(utf|UTF)(-|)8}[1]:-en_US.UTF-8} fi - # If local branch name or tag is at most 32 characters long, show it in full. - # Otherwise show the first 12 … the last 12. - (( $#where > 50 )) && where[13,-13]="…" - res+="${clean}${where//\%/%%}" # escape % - - # Display the current Git commit if there is no branch or tag. - # Tip: To always display the current Git commit, remove `[[ -z $where ]] &&` from the next line. - [[ -z $where ]] && res+="${meta}@${clean}${VCS_STATUS_COMMIT[1,8]}" - - # Show tracking branch name if it differs from local branch. - if [[ -n ${VCS_STATUS_REMOTE_BRANCH:#$VCS_STATUS_LOCAL_BRANCH} ]]; then - res+="${meta}:${clean}${(V)VCS_STATUS_REMOTE_BRANCH//\%/%%}" # escape % + # The list of segments shown on the left. Fill it with the most important segments. + typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=( + # =========================[ Line #1 ]========================= + # os_icon # os identifier + dir # current directory + vcs # git status + # =========================[ Line #2 ]========================= + newline # \n + # prompt_char # prompt symbol + ) + + # The list of segments shown on the right. Fill it with less important segments. + # Right prompt on the last prompt line (where you are typing your commands) gets + # automatically hidden when the input line reaches it. Right prompt above the + # last prompt line gets hidden if it would overlap with left prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( + # =========================[ Line #1 ]========================= + status # exit code of the last command + command_execution_time # duration of the last command + background_jobs # presence of background jobs + direnv # direnv status (https://direnv.net/) + asdf # asdf version manager (https://github.com/asdf-vm/asdf) + virtualenv # python virtual environment (https://docs.python.org/3/library/venv.html) + anaconda # conda environment (https://conda.io/) + pyenv # python environment (https://github.com/pyenv/pyenv) + goenv # go environment (https://github.com/syndbg/goenv) + nodenv # node.js version from nodenv (https://github.com/nodenv/nodenv) + nvm # node.js version from nvm (https://github.com/nvm-sh/nvm) + nodeenv # node.js environment (https://github.com/ekalinin/nodeenv) + # node_version # node.js version + # go_version # go version (https://golang.org) + # rust_version # rustc version (https://www.rust-lang.org) + # dotnet_version # .NET version (https://dotnet.microsoft.com) + rbenv # ruby version from rbenv (https://github.com/rbenv/rbenv) + rvm # ruby version from rvm (https://rvm.io) + fvm # flutter version management (https://github.com/leoafarias/fvm) + luaenv # lua version from luaenv (https://github.com/cehoffman/luaenv) + jenv # java version from jenv (https://github.com/jenv/jenv) + plenv # perl version from plenv (https://github.com/tokuhirom/plenv) + kubecontext # current kubernetes context (https://kubernetes.io/) + terraform # terraform workspace (https://www.terraform.io) + aws # aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) + aws_eb_env # aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) + azure # azure account name (https://docs.microsoft.com/en-us/cli/azure) + gcloud # google cloud cli account and project (https://cloud.google.com/) + google_app_cred # google application credentials (https://cloud.google.com/docs/authentication/production) + context # user@hostname + nordvpn # nordvpn connection status, linux only (https://nordvpn.com/) + ranger # ranger shell (https://github.com/ranger/ranger) + nnn # nnn shell (https://github.com/jarun/nnn) + vim_shell # vim shell indicator (:sh) + midnight_commander # midnight commander shell (https://midnight-commander.org/) + nix_shell # nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) + vi_mode # vi mode (you don't need this if you've enabled prompt_char) + # load # CPU load + # disk_usage # disk usage + # ram # free RAM + # swap # used swap + todo # todo items (https://github.com/todotxt/todo.txt-cli) + timewarrior # timewarrior tracking status (https://timewarrior.net/) + time # current time + # =========================[ Line #2 ]========================= + newline # \n + # ip # ip address and bandwidth usage for a specified network interface + # public_ip # public IP address + # proxy # system-wide http/https/ftp proxy + # battery # internal battery + # wifi # wifi speed + # example # example user-defined segment (see prompt_example function below) + ) + + # To enable default icons for all segments, don't define POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION + # or set it to '${P9K_VISUAL_IDENTIFIER}'. + # + # To remove trailing space from all default icons, set POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION + # to '${P9K_VISUAL_IDENTIFIER% }'. + # + # To enable default icons for one segment (e.g., dir), set + # POWERLEVEL9K_DIR_VISUAL_IDENTIFIER_EXPANSION='${P9K_VISUAL_IDENTIFIER}'. + # + # To assign a specific icon to one segment (e.g., dir), set + # POWERLEVEL9K_DIR_VISUAL_IDENTIFIER_EXPANSION='⭐'. + # + # To assign a specific icon to a segment in a given state (e.g., dir in state NOT_WRITABLE), + # set POWERLEVEL9K_DIR_NOT_WRITABLE_VISUAL_IDENTIFIER_EXPANSION='⭐'. + # + # Note: You can use $'\u2B50' instead of '⭐'. It's especially convenient when specifying + # icons that your text editor cannot render. Don't forget to put $ and use single quotes when + # defining icons via Unicode codepoints. + # + # Note: Many default icons cannot be displayed with system fonts. You'll need to install a + # capable font to use them. See POWERLEVEL9K_MODE below. + typeset -g POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION='${P9K_VISUAL_IDENTIFIER}' + + # This option makes a difference only when default icons are enabled for all or some prompt + # segments (see POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION above). LOCK_ICON can be printed as + # $'\uE0A2', $'\uE138' or $'\uF023' depending on POWERLEVEL9K_MODE. The correct value of this + # parameter depends on the provider of the font your terminal is using. + # + # Font Provider | POWERLEVEL9K_MODE + # ---------------------------------+------------------- + # Powerline | powerline + # Font Awesome | awesome-fontconfig + # Adobe Source Code Pro | awesome-fontconfig + # Source Code Pro | awesome-fontconfig + # Awesome-Terminal Fonts (regular) | awesome-fontconfig + # Awesome-Terminal Fonts (patched) | awesome-patched + # Nerd Fonts | nerdfont-complete + # Other | compatible + # + # If this looks overwhelming, either stick with a preinstalled system font and set + # POWERLEVEL9K_MODE=compatible, or install the recommended Powerlevel10k font from + # https://github.com/romkatv/powerlevel10k/#recommended-meslo-nerd-font-patched-for-powerlevel10k + # and set POWERLEVEL9K_MODE=nerdfont-complete. + typeset -g POWERLEVEL9K_MODE=compatible + + # When set to true, icons appear before content on both sides of the prompt. When set + # to false, icons go after content. If empty or not set, icons go before content in the left + # prompt and after content in the right prompt. + # + # You can also override it for a specific segment: + # + # POWERLEVEL9K_STATUS_ICON_BEFORE_CONTENT=false + # + # Or for a specific segment in specific state: + # + # POWERLEVEL9K_DIR_NOT_WRITABLE_ICON_BEFORE_CONTENT=false + typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT= + + # Add an empty line before each prompt. + typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=true + + # Connect left prompt lines with these symbols. You'll probably want to use the same color + # as POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND below. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_PREFIX='%242F╭─' + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_PREFIX='%242F├─' + typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_PREFIX='%242F╰─' + # Connect right prompt lines with these symbols. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_SUFFIX='%242F─╮' + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_SUFFIX='%242F─┤' + typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_SUFFIX='%242F─╯' + + # Filler between left and right prompt on the first prompt line. You can set it to ' ', '·' or + # '─'. The last two make it easier to see the alignment between left and right prompt and to + # separate prompt from command output. You might want to set POWERLEVEL9K_PROMPT_ADD_NEWLINE=false + # for more compact prompt if using using this option. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR=' ' + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_BACKGROUND= + if [[ $POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR != ' ' ]]; then + # The color of the filler. You'll probably want to match the color of POWERLEVEL9K_MULTILINE + # ornaments defined above. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND=242 + # Start filler from the edge of the screen if there are no left segments on the first line. + typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_FIRST_SEGMENT_END_SYMBOL='%{%}' + # End filler on the edge of the screen if there are no right segments on the first line. + typeset -g POWERLEVEL9K_EMPTY_LINE_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='%{%}' fi - # ⇣42 if behind the remote. - (( VCS_STATUS_COMMITS_BEHIND )) && res+=" ${clean}⇣${VCS_STATUS_COMMITS_BEHIND}" - # ⇡42 if ahead of the remote; no leading space if also behind the remote: ⇣42⇡42. - (( VCS_STATUS_COMMITS_AHEAD && !VCS_STATUS_COMMITS_BEHIND )) && res+=" " - (( VCS_STATUS_COMMITS_AHEAD )) && res+="${clean}⇡${VCS_STATUS_COMMITS_AHEAD}" - # ⇠42 if behind the push remote. - (( VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" ${clean}⇠${VCS_STATUS_PUSH_COMMITS_BEHIND}" - (( VCS_STATUS_PUSH_COMMITS_AHEAD && !VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" " - # ⇢42 if ahead of the push remote; no leading space if also behind: ⇠42⇢42. - (( VCS_STATUS_PUSH_COMMITS_AHEAD )) && res+="${clean}⇢${VCS_STATUS_PUSH_COMMITS_AHEAD}" - # *42 if have stashes. - (( VCS_STATUS_STASHES )) && res+=" ${clean}*${VCS_STATUS_STASHES}" - # 'merge' if the repo is in an unusual state. - [[ -n $VCS_STATUS_ACTION ]] && res+=" ${conflicted}${VCS_STATUS_ACTION}" - # ~42 if have merge conflicts. - (( VCS_STATUS_NUM_CONFLICTED )) && res+=" ${conflicted}~${VCS_STATUS_NUM_CONFLICTED}" - # +42 if have staged changes. - (( VCS_STATUS_NUM_STAGED )) && res+=" ${modified}+${VCS_STATUS_NUM_STAGED}" - # !42 if have unstaged changes. - (( VCS_STATUS_NUM_UNSTAGED )) && res+=" ${modified}!${VCS_STATUS_NUM_UNSTAGED}" - # ?42 if have untracked files. It's really a question mark, your font isn't broken. - # See POWERLEVEL9K_VCS_UNTRACKED_ICON above if you want to use a different icon. - # Remove the next line if you don't want to see untracked files at all. - (( VCS_STATUS_NUM_UNTRACKED )) && res+=" ${untracked}${POWERLEVEL9K_VCS_UNTRACKED_ICON}${VCS_STATUS_NUM_UNTRACKED}" - # "─" if the number of unstaged files is unknown. This can happen due to - # POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY (see below) being set to a non-negative number lower - # than the number of files in the Git index, or due to bash.showDirtyState being set to false - # in the repository config. The number of staged and untracked files may also be unknown - # in this case. - (( VCS_STATUS_HAS_UNSTAGED == -1 )) && res+=" ${modified}─" - - typeset -g my_git_format=$res - } - functions -M my_git_formatter 2>/dev/null - - # Don't count the number of unstaged, untracked and conflicted files in Git repositories with - # more than this many files in the index. Negative value means infinity. - # - # If you are working in Git repositories with tens of millions of files and seeing performance - # sagging, try setting POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY to a number lower than the output - # of `git ls-files | wc -l`. Alternatively, add `bash.showDirtyState = false` to the repository's - # config: `git config bash.showDirtyState false`. - typeset -g POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY=-1 - - # Don't show Git status in prompt for repositories whose workdir matches this pattern. - # For example, if set to '~', the Git repository at $HOME/.git will be ignored. - # Multiple patterns can be combined with '|': '~|~/some/dir'. - typeset -g POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN='~' - - # Disable the default Git status formatting. - typeset -g POWERLEVEL9K_VCS_DISABLE_GITSTATUS_FORMATTING=true - # Install our own Git status formatter. - typeset -g POWERLEVEL9K_VCS_CONTENT_EXPANSION='${$((my_git_formatter(1)))+${my_git_format}}' - typeset -g POWERLEVEL9K_VCS_LOADING_CONTENT_EXPANSION='${$((my_git_formatter(0)))+${my_git_format}}' - # Enable counters for staged, unstaged, etc. - typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1 - - # Icon color. - typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_COLOR=76 - typeset -g POWERLEVEL9K_VCS_LOADING_VISUAL_IDENTIFIER_COLOR=244 - # Custom icon. - typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_EXPANSION= - # Custom prefix. - typeset -g POWERLEVEL9K_VCS_PREFIX='%248Fon ' - - # Show status of repositories of these types. You can add svn and/or hg if you are - # using them. If you do, your prompt may become slow even when your current directory - # isn't in an svn or hg reposotiry. - typeset -g POWERLEVEL9K_VCS_BACKENDS=(git) - - # These settings are used for repositories other than Git or when gitstatusd fails and - # Powerlevel10k has to fall back to using vcs_info. - typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND=76 - typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND=76 - typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND=178 - - ##########################[ status: exit code of the last command ]########################### - # Enable OK_PIPE, ERROR_PIPE and ERROR_SIGNAL status states to allow us to enable, disable and - # style them independently from the regular OK and ERROR state. - typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true - - # Status on success. No content, just an icon. No need to show it if prompt_char is enabled as - # it will signify success by turning green. - typeset -g POWERLEVEL9K_STATUS_OK=true - typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND=70 - typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔' - - # Status when some part of a pipe command fails but the overall exit status is zero. It may look - # like this: 1|0. - typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true - typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND=70 - typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔' - - # Status when it's just an error code (e.g., '1'). No need to show it if prompt_char is enabled as - # it will signify error by turning red. - typeset -g POWERLEVEL9K_STATUS_ERROR=true - typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND=160 - typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='х' - - # Status when the last command was terminated by a signal. - typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true - typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND=160 - # Use terse signal names: "INT" instead of "SIGINT(2)". - typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false - typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='х' - - # Status when some part of a pipe command fails and the overall exit status is also non-zero. - # It may look like this: 1|0. - typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true - typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND=160 - typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='х' - - ###################[ command_execution_time: duration of the last command ]################### - # Show duration of the last command if takes longer than this many seconds. - typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3 - # Show this many fractional digits. Zero means round to seconds. - typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0 - # Execution time color. - typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=248 - # Duration format: 1d 2h 3m 4s. - typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s' - # Custom icon. - typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_VISUAL_IDENTIFIER_EXPANSION= - # Custom prefix. - typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PREFIX='%248Ftook ' - - #######################[ background_jobs: presence of background jobs ]####################### - # Don't show the number of background jobs. - typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false - # Background jobs color. - typeset -g POWERLEVEL9K_BACKGROUND_JOBS_FOREGROUND=37 - # Custom icon. - typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VISUAL_IDENTIFIER_EXPANSION='≡' - - #######################[ direnv: direnv status (https://direnv.net/) ]######################## - # Direnv color. - typeset -g POWERLEVEL9K_DIRENV_FOREGROUND=178 - # Custom icon. - # typeset -g POWERLEVEL9K_DIRENV_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ###############[ asdf: asdf version manager (https://github.com/asdf-vm/asdf) ]############### - # Default asdf color. Only used to display tools for which there is no color override (see below). - typeset -g POWERLEVEL9K_ASDF_FOREGROUND=66 - - # There are three parameters that can be used to hide tools. If at least one of them decides - # to hide a tool, that tool gets hidden. POWERLEVEL9K_ASDF_SHOW_SYSTEM=false hides "system". To - # see the difference between POWERLEVEL9K_ASDF_SOURCES and POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW - # consider the effect of the following commands: - # - # asdf local python 3.8.1 - # asdf global python 3.8.1 - # - # After running both commands the current python version is 3.8.1 and its source is "local" as - # it takes precedence over "global". If POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW is set to false, - # it'll hide python version in this case because 3.8.1 is the same as the global version. - # POWERLEVEL9K_ASDF_SOURCES will hide python version only if the value of this parameter doesn't - # contain "local". - - # Hide tool versions that don't come from one of these sources. - # - # Available sources: - # - # - shell `asdf current` says "set by ASDF_${TOOL}_VERSION environment variable" - # - local `asdf current` says "set by /some/not/home/directory/file" - # - global `asdf current` says "set by /home/username/file" - # - # Note: If this parameter is set to (shell local global), it won't hide tools. - # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SOURCES. - typeset -g POWERLEVEL9K_ASDF_SOURCES=(shell local global) - - # If set to false, hide tool versions that are the same as global. - # - # Note: The name of this parameter doesn't reflect its meaning at all. - # Note: If this parameter is set to true, it won't hide tools. - # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_PROMPT_ALWAYS_SHOW. - typeset -g POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW=false - - # If set to false, hide tool versions that are equal to "system". - # - # Note: If this parameter is set to true, it won't hide tools. - # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SHOW_SYSTEM. - typeset -g POWERLEVEL9K_ASDF_SHOW_SYSTEM=true - - # Ruby version from asdf. - typeset -g POWERLEVEL9K_ASDF_RUBY_FOREGROUND=168 - # typeset -g POWERLEVEL9K_ASDF_RUBY_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Python version from asdf. - typeset -g POWERLEVEL9K_ASDF_PYTHON_FOREGROUND=37 - # typeset -g POWERLEVEL9K_ASDF_PYTHON_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Go version from asdf. - typeset -g POWERLEVEL9K_ASDF_GO_FOREGROUND=37 - # typeset -g POWERLEVEL9K_ASDF_GO_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Node.js version from asdf. - typeset -g POWERLEVEL9K_ASDF_NODEJS_FOREGROUND=70 - # typeset -g POWERLEVEL9K_ASDF_NODEJS_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Rust version from asdf. - typeset -g POWERLEVEL9K_ASDF_RUST_FOREGROUND=37 - # typeset -g POWERLEVEL9K_ASDF_RUST_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # .NET Core version from asdf. - typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_FOREGROUND=134 - # typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Flutter version from asdf. - typeset -g POWERLEVEL9K_ASDF_FLUTTER_FOREGROUND=38 - # typeset -g POWERLEVEL9K_ASDF_FLUTTER_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Lua version from asdf. - typeset -g POWERLEVEL9K_ASDF_LUA_FOREGROUND=32 - # typeset -g POWERLEVEL9K_ASDF_LUA_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Java version from asdf. - typeset -g POWERLEVEL9K_ASDF_JAVA_FOREGROUND=32 - # typeset -g POWERLEVEL9K_ASDF_JAVA_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Perl version from asdf. - typeset -g POWERLEVEL9K_ASDF_PERL_FOREGROUND=67 - # typeset -g POWERLEVEL9K_ASDF_PERL_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Erlang version from asdf. - typeset -g POWERLEVEL9K_ASDF_ERLANG_FOREGROUND=125 - # typeset -g POWERLEVEL9K_ASDF_ERLANG_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Elixir version from asdf. - typeset -g POWERLEVEL9K_ASDF_ELIXIR_FOREGROUND=129 - # typeset -g POWERLEVEL9K_ASDF_ELIXIR_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Postgres version from asdf. - typeset -g POWERLEVEL9K_ASDF_POSTGRES_FOREGROUND=31 - # typeset -g POWERLEVEL9K_ASDF_POSTGRES_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ##########[ nordvpn: nordvpn connection status, linux only (https://nordvpn.com/) ]########### - # NordVPN connection indicator color. - typeset -g POWERLEVEL9K_NORDVPN_FOREGROUND=39 - # Hide NordVPN connection indicator when not connected. - typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_CONTENT_EXPANSION= - typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_VISUAL_IDENTIFIER_EXPANSION= - # Custom icon. - # typeset -g POWERLEVEL9K_NORDVPN_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #################[ ranger: ranger shell (https://github.com/ranger/ranger) ]################## - # Ranger shell color. - typeset -g POWERLEVEL9K_RANGER_FOREGROUND=178 - # Custom icon. - typeset -g POWERLEVEL9K_RANGER_VISUAL_IDENTIFIER_EXPANSION='▲' - - ######################[ nnn: nnn shell (https://github.com/jarun/nnn) ]####################### - # Nnn shell color. - typeset -g POWERLEVEL9K_NNN_FOREGROUND=72 - # Custom icon. - # typeset -g POWERLEVEL9K_NNN_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ###########################[ vim_shell: vim shell indicator (:sh) ]########################### - # Vim shell indicator color. - typeset -g POWERLEVEL9K_VIM_SHELL_FOREGROUND=34 - # Custom icon. - # typeset -g POWERLEVEL9K_VIM_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ######[ midnight_commander: midnight commander shell (https://midnight-commander.org/) ]###### - # Midnight Commander shell color. - typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_FOREGROUND=178 - # Custom icon. - # typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #[ nix_shell: nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) ]## - # Nix shell color. - typeset -g POWERLEVEL9K_NIX_SHELL_FOREGROUND=74 - - # Tip: If you want to see just the icon without "pure" and "impure", uncomment the next line. - # typeset -g POWERLEVEL9K_NIX_SHELL_CONTENT_EXPANSION= - - # Custom icon. - # typeset -g POWERLEVEL9K_NIX_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ##################################[ disk_usgae: disk usage ]################################## - # Colors for different levels of disk usage. - typeset -g POWERLEVEL9K_DISK_USAGE_NORMAL_FOREGROUND=35 - typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_FOREGROUND=220 - typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_FOREGROUND=160 - # Thresholds for different levels of disk usage (percentage points). - typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL=90 - typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_LEVEL=95 - # If set to true, hide disk usage when below $POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL percent. - typeset -g POWERLEVEL9K_DISK_USAGE_ONLY_WARNING=false - # Custom icon. - # typeset -g POWERLEVEL9K_DISK_USAGE_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ###########[ vi_mode: vi mode (you don't need this if you've enabled prompt_char) ]########### - # Text and color for normal (a.k.a. command) vi mode. - typeset -g POWERLEVEL9K_VI_COMMAND_MODE_STRING=NORMAL - typeset -g POWERLEVEL9K_VI_MODE_NORMAL_FOREGROUND=106 - # Text and color for visual vi mode. - typeset -g POWERLEVEL9K_VI_VISUAL_MODE_STRING=VISUAL - typeset -g POWERLEVEL9K_VI_MODE_VISUAL_FOREGROUND=68 - # Text and color for overtype (a.k.a. overwrite and replace) vi mode. - typeset -g POWERLEVEL9K_VI_OVERWRITE_MODE_STRING=OVERTYPE - typeset -g POWERLEVEL9K_VI_MODE_OVERWRITE_FOREGROUND=172 - # Text and color for insert vi mode. - typeset -g POWERLEVEL9K_VI_INSERT_MODE_STRING= - typeset -g POWERLEVEL9K_VI_MODE_INSERT_FOREGROUND=66 - - # Custom icon. - typeset -g POWERLEVEL9K_RANGER_VISUAL_IDENTIFIER_EXPANSION='▲' - - ######################################[ ram: free RAM ]####################################### - # RAM color. - typeset -g POWERLEVEL9K_RAM_FOREGROUND=66 - # Custom icon. - # typeset -g POWERLEVEL9K_RAM_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #####################################[ swap: used swap ]###################################### - # Swap color. - typeset -g POWERLEVEL9K_SWAP_FOREGROUND=96 - # Custom icon. - # typeset -g POWERLEVEL9K_SWAP_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ######################################[ load: CPU load ]###################################### - # Show average CPU load over this many last minutes. Valid values are 1, 5 and 15. - typeset -g POWERLEVEL9K_LOAD_WHICH=5 - # Load color when load is under 50%. - typeset -g POWERLEVEL9K_LOAD_NORMAL_FOREGROUND=66 - # Load color when load is between 50% and 70%. - typeset -g POWERLEVEL9K_LOAD_WARNING_FOREGROUND=178 - # Load color when load is over 70%. - typeset -g POWERLEVEL9K_LOAD_CRITICAL_FOREGROUND=166 - # Custom icon. - # typeset -g POWERLEVEL9K_LOAD_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ################[ todo: todo items (https://github.com/todotxt/todo.txt-cli) ]################ - # Todo color. - typeset -g POWERLEVEL9K_TODO_FOREGROUND=110 - # Hide todo when the total number of tasks is zero. - typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_TOTAL=true - # Hide todo when the number of tasks after filtering is zero. - typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_FILTERED=false - - # Todo format. The following parameters are available within the expansion. - # - # - P9K_TODO_TOTAL_TASK_COUNT The total number of tasks. - # - P9K_TODO_FILTERED_TASK_COUNT The number of tasks after filtering. - # - # These variables correspond to the last line of the output of `todo.sh -p ls`: - # - # TODO: 24 of 42 tasks shown - # - # Here 24 is P9K_TODO_FILTERED_TASK_COUNT and 42 is P9K_TODO_TOTAL_TASK_COUNT. - # - # typeset -g POWERLEVEL9K_TODO_CONTENT_EXPANSION='$P9K_TODO_FILTERED_TASK_COUNT' - - # Custom icon. - # typeset -g POWERLEVEL9K_TODO_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ###########[ timewarrior: timewarrior tracking status (https://timewarrior.net/) ]############ - # Timewarrior color. - typeset -g POWERLEVEL9K_TIMEWARRIOR_FOREGROUND=110 - # If the tracked task is longer than 24 characters, truncate and append "…". - # Tip: To always display tasks without truncation, delete the following parameter. - # Tip: To hide task names and display just the icon when time tracking is enabled, set the - # value of the following parameter to "". - typeset -g POWERLEVEL9K_TIMEWARRIOR_CONTENT_EXPANSION='${P9K_CONTENT:0:24}${${P9K_CONTENT:24}:+…}' - - # Custom icon. - # typeset -g POWERLEVEL9K_TIMEWARRIOR_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ##################################[ context: user@hostname ]################################## - # Context color when running with privileges. - typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=178 - # Context color in SSH without privileges. - typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_FOREGROUND=180 - # Default context color (no privileges, no SSH). - typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=180 - - # Context format when running with privileges: bold user@hostname. - typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%B%n@%m' - # Context format when in SSH without privileges: user@hostname. - typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_TEMPLATE='%n@%m' - # Default context format (no privileges, no SSH): user@hostname. - typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE='%n@%m' - - # Don't show context unless running with privileges or in SSH. - # Tip: Remove the next line to always show context. - typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_{CONTENT,VISUAL_IDENTIFIER}_EXPANSION= - - # Custom icon. - # typeset -g POWERLEVEL9K_CONTEXT_VISUAL_IDENTIFIER_EXPANSION='⭐' - # Custom prefix. - typeset -g POWERLEVEL9K_CONTEXT_PREFIX='%248Fwith ' - - ###[ virtualenv: python virtual environment (https://docs.python.org/3/library/venv.html) ]### - # Python virtual environment color. - typeset -g POWERLEVEL9K_VIRTUALENV_FOREGROUND=37 - # Don't show Python version next to the virtual environment name. - typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_PYTHON_VERSION=false - # Separate environment name from Python version only with a space. - typeset -g POWERLEVEL9K_VIRTUALENV_{LEFT,RIGHT}_DELIMITER= - # Custom icon. - # typeset -g POWERLEVEL9K_VIRTUALENV_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #####################[ anaconda: conda environment (https://conda.io/) ]###################### - # Anaconda environment color. - typeset -g POWERLEVEL9K_ANACONDA_FOREGROUND=37 - # Don't show Python version next to the anaconda environment name. - typeset -g POWERLEVEL9K_ANACONDA_SHOW_PYTHON_VERSION=false - # Separate environment name from Python version only with a space. - typeset -g POWERLEVEL9K_ANACONDA_{LEFT,RIGHT}_DELIMITER= - # Custom icon. - # typeset -g POWERLEVEL9K_ANACONDA_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ################[ pyenv: python environment (https://github.com/pyenv/pyenv) ]################ - # Pyenv color. - typeset -g POWERLEVEL9K_PYENV_FOREGROUND=37 - # Hide python version if it doesn't come from one of these sources. - typeset -g POWERLEVEL9K_PYENV_SOURCES=(shell local global) - # If set to false, hide python version if it's the same as global: - # $(pyenv version-name) == $(pyenv global). - typeset -g POWERLEVEL9K_PYENV_PROMPT_ALWAYS_SHOW=false - # Custom icon. - # typeset -g POWERLEVEL9K_PYENV_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ################[ goenv: go environment (https://github.com/syndbg/goenv) ]################ - # Goenv color. - typeset -g POWERLEVEL9K_GOENV_FOREGROUND=37 - # Hide go version if it doesn't come from one of these sources. - typeset -g POWERLEVEL9K_GOENV_SOURCES=(shell local global) - # If set to false, hide go version if it's the same as global: - # $(goenv version-name) == $(goenv global). - typeset -g POWERLEVEL9K_GOENV_PROMPT_ALWAYS_SHOW=false - # Custom icon. - # typeset -g POWERLEVEL9K_GOENV_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ##########[ nodenv: node.js version from nodenv (https://github.com/nodenv/nodenv) ]########## - # Nodenv color. - typeset -g POWERLEVEL9K_NODENV_FOREGROUND=70 - # Don't show node version if it's the same as global: $(nodenv version-name) == $(nodenv global). - typeset -g POWERLEVEL9K_NODENV_PROMPT_ALWAYS_SHOW=false - # Custom icon. - # typeset -g POWERLEVEL9K_NODENV_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ##############[ nvm: node.js version from nvm (https://github.com/nvm-sh/nvm) ]############### - # Nvm color. - typeset -g POWERLEVEL9K_NVM_FOREGROUND=70 - # Custom icon. - # typeset -g POWERLEVEL9K_NVM_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ############[ nodeenv: node.js environment (https://github.com/ekalinin/nodeenv) ]############ - # Nodeenv color. - typeset -g POWERLEVEL9K_NODEENV_FOREGROUND=70 - # Don't show Node version next to the environment name. - typeset -g POWERLEVEL9K_NODEENV_SHOW_NODE_VERSION=false - # Separate environment name from Node version only with a space. - typeset -g POWERLEVEL9K_NODEENV_{LEFT,RIGHT}_DELIMITER= - # Custom icon. - # typeset -g POWERLEVEL9K_NODEENV_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ##############################[ node_version: node.js version ]############################### - # Node version color. - typeset -g POWERLEVEL9K_NODE_VERSION_FOREGROUND=70 - # Show node version only when in a directory tree containing package.json. - typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true - # Custom icon. - # typeset -g POWERLEVEL9K_NODE_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #######################[ go_version: go version (https://golang.org) ]######################## - # Go version color. - typeset -g POWERLEVEL9K_GO_VERSION_FOREGROUND=37 - # Show go version only when in a go project subdirectory. - typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true - # Custom icon. - # typeset -g POWERLEVEL9K_GO_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #################[ rust_version: rustc version (https://www.rust-lang.org) ]################## - # Rust version color. - typeset -g POWERLEVEL9K_RUST_VERSION_FOREGROUND=37 - # Show rust version only when in a rust project subdirectory. - typeset -g POWERLEVEL9K_RUST_VERSION_PROJECT_ONLY=true - # Custom icon. - # typeset -g POWERLEVEL9K_RUST_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ###############[ dotnet_version: .NET version (https://dotnet.microsoft.com) ]################ - # .NET version color. - typeset -g POWERLEVEL9K_DOTNET_VERSION_FOREGROUND=134 - # Show .NET version only when in a .NET project subdirectory. - typeset -g POWERLEVEL9K_DOTNET_VERSION_PROJECT_ONLY=true - # Custom icon. - # typeset -g POWERLEVEL9K_DOTNET_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #############[ rbenv: ruby version from rbenv (https://github.com/rbenv/rbenv) ]############## - # Rbenv color. - typeset -g POWERLEVEL9K_RBENV_FOREGROUND=168 - # Hide ruby version if it doesn't come from one of these sources. - typeset -g POWERLEVEL9K_RBENV_SOURCES=(shell local global) - # If set to false, hide ruby version if it's the same as global: - # $(rbenv version-name) == $(rbenv global). - typeset -g POWERLEVEL9K_RBENV_PROMPT_ALWAYS_SHOW=false - # Custom icon. - # typeset -g POWERLEVEL9K_RBENV_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #######################[ rvm: ruby version from rvm (https://rvm.io) ]######################## - # Rvm color. - typeset -g POWERLEVEL9K_RVM_FOREGROUND=168 - # Don't show @gemset at the end. - typeset -g POWERLEVEL9K_RVM_SHOW_GEMSET=false - # Don't show ruby- at the front. - typeset -g POWERLEVEL9K_RVM_SHOW_PREFIX=false - # Custom icon. - # typeset -g POWERLEVEL9K_RVM_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ###########[ fvm: flutter version management (https://github.com/leoafarias/fvm) ]############ - # Fvm color. - typeset -g POWERLEVEL9K_FVM_FOREGROUND=38 - # Custom icon. - # typeset -g POWERLEVEL9K_FVM_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ##########[ luaenv: lua version from luaenv (https://github.com/cehoffman/luaenv) ]########### - # Lua color. - typeset -g POWERLEVEL9K_LUAENV_FOREGROUND=32 - # Hide lua version if it doesn't come from one of these sources. - typeset -g POWERLEVEL9K_LUAENV_SOURCES=(shell local global) - # If set to false, hide lua version if it's the same as global: - # $(luaenv version-name) == $(luaenv global). - typeset -g POWERLEVEL9K_LUAENV_PROMPT_ALWAYS_SHOW=false - # Custom icon. - # typeset -g POWERLEVEL9K_LUAENV_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ###############[ jenv: java version from jenv (https://github.com/jenv/jenv) ]################ - # Java color. - typeset -g POWERLEVEL9K_JENV_FOREGROUND=32 - # Hide java version if it doesn't come from one of these sources. - typeset -g POWERLEVEL9K_JENV_SOURCES=(shell local global) - # If set to false, hide java version if it's the same as global: - # $(jenv version-name) == $(jenv global). - typeset -g POWERLEVEL9K_JENV_PROMPT_ALWAYS_SHOW=false - # Custom icon. - # typeset -g POWERLEVEL9K_JENV_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ###########[ plenv: perl version from plenv (https://github.com/tokuhirom/plenv) ]############ - # Perl color. - typeset -g POWERLEVEL9K_PLENV_FOREGROUND=67 - # Hide perl version if it doesn't come from one of these sources. - typeset -g POWERLEVEL9K_PLENV_SOURCES=(shell local global) - # If set to false, hide perl version if it's the same as global: - # $(plenv version-name) == $(plenv global). - typeset -g POWERLEVEL9K_PLENV_PROMPT_ALWAYS_SHOW=false - # Custom icon. - # typeset -g POWERLEVEL9K_PLENV_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ################[ terraform: terraform workspace (https://www.terraform.io) ]################# - # POWERLEVEL9K_TERRAFORM_CLASSES is an array with even number of elements. The first element - # in each pair defines a pattern against which the current terraform workspace gets matched. - # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) - # that gets matched. If you unset all POWERLEVEL9K_TERRAFORM_*CONTENT_EXPANSION parameters, - # you'll see this value in your prompt. The second element of each pair in - # POWERLEVEL9K_TERRAFORM_CLASSES defines the workspace class. Patterns are tried in order. The - # first match wins. - # - # For example, given these settings: - # - # typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( - # '*prod*' PROD - # '*test*' TEST - # '*' DEFAULT) - # - # If your current terraform workspace is "project_test", its class is TEST because "project_test" - # doesn't match the pattern '*prod*' but does match '*test*'. - # - # You can define different colors, icons and content expansions for different classes: - # - # typeset -g POWERLEVEL9K_TERRAFORM_TEST_FOREGROUND=28 - # typeset -g POWERLEVEL9K_TERRAFORM_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' - # typeset -g POWERLEVEL9K_TERRAFORM_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' - typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( - # '*prod*' PROD # These values are examples that are unlikely - # '*test*' TEST # to match your needs. Customize them as needed. - '*' DEFAULT) - typeset -g POWERLEVEL9K_TERRAFORM_DEFAULT_FOREGROUND=38 - # typeset -g POWERLEVEL9K_TERRAFORM_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #############[ kubecontext: current kubernetes context (https://kubernetes.io/) ]############# - # Show kubecontext only when the the command you are typing invokes one of these tools. - # Tip: Remove the next line to always show kubecontext. - typeset -g POWERLEVEL9K_KUBECONTEXT_SHOW_ON_COMMAND='kubectl|helm|kubens|kubectx|oc' - - # Kubernetes context classes for the purpose of using different colors, icons and expansions with - # different contexts. - # - # POWERLEVEL9K_KUBECONTEXT_CLASSES is an array with even number of elements. The first element - # in each pair defines a pattern against which the current kubernetes context gets matched. - # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) - # that gets matched. If you unset all POWERLEVEL9K_KUBECONTEXT_*CONTENT_EXPANSION parameters, - # you'll see this value in your prompt. The second element of each pair in - # POWERLEVEL9K_KUBECONTEXT_CLASSES defines the context class. Patterns are tried in order. The - # first match wins. - # - # For example, given these settings: - # - # typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( - # '*prod*' PROD - # '*test*' TEST - # '*' DEFAULT) - # - # If your current kubernetes context is "deathray-testing/default", its class is TEST - # because "deathray-testing/default" doesn't match the pattern '*prod*' but does match '*test*'. - # - # You can define different colors, icons and content expansions for different classes: - # - # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_FOREGROUND=28 - # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' - # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' - typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( - # '*prod*' PROD # These values are examples that are unlikely - # '*test*' TEST # to match your needs. Customize them as needed. - '*' DEFAULT) - typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_FOREGROUND=134 - typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='○' - - # Use POWERLEVEL9K_KUBECONTEXT_CONTENT_EXPANSION to specify the content displayed by kubecontext - # segment. Parameter expansions are very flexible and fast, too. See reference: - # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. - # - # Within the expansion the following parameters are always available: - # - # - P9K_CONTENT The content that would've been displayed if there was no content - # expansion defined. - # - P9K_KUBECONTEXT_NAME The current context's name. Corresponds to column NAME in the - # output of `kubectl config get-contexts`. - # - P9K_KUBECONTEXT_CLUSTER The current context's cluster. Corresponds to column CLUSTER in the - # output of `kubectl config get-contexts`. - # - P9K_KUBECONTEXT_NAMESPACE The current context's namespace. Corresponds to column NAMESPACE - # in the output of `kubectl config get-contexts`. If there is no - # namespace, the parameter is set to "default". - # - P9K_KUBECONTEXT_USER The current context's user. Corresponds to column AUTHINFO in the - # output of `kubectl config get-contexts`. - # - # If the context points to Google Kubernetes Engine (GKE) or Elastic Kubernetes Service (EKS), - # the following extra parameters are available: - # - # - P9K_KUBECONTEXT_CLOUD_NAME Either "gke" or "eks". - # - P9K_KUBECONTEXT_CLOUD_ACCOUNT Account/project ID. - # - P9K_KUBECONTEXT_CLOUD_ZONE Availability zone. - # - P9K_KUBECONTEXT_CLOUD_CLUSTER Cluster. - # - # P9K_KUBECONTEXT_CLOUD_* parameters are derived from P9K_KUBECONTEXT_CLUSTER. For example, - # if P9K_KUBECONTEXT_CLUSTER is "gke_my-account_us-east1-a_my-cluster-01": - # - # - P9K_KUBECONTEXT_CLOUD_NAME=gke - # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=my-account - # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east1-a - # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 - # - # If P9K_KUBECONTEXT_CLUSTER is "arn:aws:eks:us-east-1:123456789012:cluster/my-cluster-01": - # - # - P9K_KUBECONTEXT_CLOUD_NAME=eks - # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=123456789012 - # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east-1 - # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 - typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION= - # Show P9K_KUBECONTEXT_CLOUD_CLUSTER if it's not empty and fall back to P9K_KUBECONTEXT_NAME. - POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${P9K_KUBECONTEXT_CLOUD_CLUSTER:-${P9K_KUBECONTEXT_NAME}}' - # Append the current context's namespace if it's not "default". - POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${${:-/$P9K_KUBECONTEXT_NAMESPACE}:#/default}' - - # Custom prefix. - typeset -g POWERLEVEL9K_KUBECONTEXT_PREFIX='%248Fat ' - - #[ aws: aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) ]# - # Show aws only when the the command you are typing invokes one of these tools. - # Tip: Remove the next line to always show aws. - typeset -g POWERLEVEL9K_AWS_SHOW_ON_COMMAND='aws|awless|terraform|pulumi' - - # POWERLEVEL9K_AWS_CLASSES is an array with even number of elements. The first element - # in each pair defines a pattern against which the current AWS profile gets matched. - # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) - # that gets matched. If you unset all POWERLEVEL9K_AWS_*CONTENT_EXPANSION parameters, - # you'll see this value in your prompt. The second element of each pair in - # POWERLEVEL9K_AWS_CLASSES defines the profile class. Patterns are tried in order. The - # first match wins. - # - # For example, given these settings: - # - # typeset -g POWERLEVEL9K_AWS_CLASSES=( - # '*prod*' PROD - # '*test*' TEST - # '*' DEFAULT) - # - # If your current AWS profile is "company_test", its class is TEST - # because "company_test" doesn't match the pattern '*prod*' but does match '*test*'. - # - # You can define different colors, icons and content expansions for different classes: - # - # typeset -g POWERLEVEL9K_AWS_TEST_FOREGROUND=28 - # typeset -g POWERLEVEL9K_AWS_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' - # typeset -g POWERLEVEL9K_AWS_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' - typeset -g POWERLEVEL9K_AWS_CLASSES=( - # '*prod*' PROD # These values are examples that are unlikely - # '*test*' TEST # to match your needs. Customize them as needed. - '*' DEFAULT) - typeset -g POWERLEVEL9K_AWS_DEFAULT_FOREGROUND=208 - # typeset -g POWERLEVEL9K_AWS_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #[ aws_eb_env: aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) ]# - # AWS Elastic Beanstalk environment color. - typeset -g POWERLEVEL9K_AWS_EB_ENV_FOREGROUND=70 - # Custom icon. - typeset -g POWERLEVEL9K_AWS_EB_ENV_VISUAL_IDENTIFIER_EXPANSION='eb' - - ##########[ azure: azure account name (https://docs.microsoft.com/en-us/cli/azure) ]########## - # Show azure only when the the command you are typing invokes one of these tools. - # Tip: Remove the next line to always show azure. - typeset -g POWERLEVEL9K_AZURE_SHOW_ON_COMMAND='az|terraform|pulumi' - # Azure account name color. - typeset -g POWERLEVEL9K_AZURE_FOREGROUND=32 - # Custom icon. - typeset -g POWERLEVEL9K_AZURE_VISUAL_IDENTIFIER_EXPANSION='az' - - ##########[ gcloud: google cloud account and project (https://cloud.google.com/) ]########### - # Show gcloud only when the the command you are typing invokes one of these tools. - # Tip: Remove the next line to always show gcloud. - typeset -g POWERLEVEL9K_GCLOUD_SHOW_ON_COMMAND='gcloud|gcs' - # Google cloud color. - typeset -g POWERLEVEL9K_GCLOUD_FOREGROUND=32 - - # Google cloud format. Change the value of POWERLEVEL9K_GCLOUD_CONTENT_EXPANSION if the default - # is too verbose or not informative enough. - # - # P9K_GCLOUD_ACCOUNT: the output of `gcloud config get-value account` - # P9K_GCLOUD_PROJECT: the output of `gcloud config get-value project` - # ${VARIABLE//\%/%%}: ${VARIABLE} with all occurrences of '%' replaced with '%%'. - # - typeset -g POWERLEVEL9K_GCLOUD_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT//\%/%%}' - - # Custom icon. - # typeset -g POWERLEVEL9K_GCLOUD_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #[ google_app_cred: google application credentials (https://cloud.google.com/docs/authentication/production) ]# - # Show google_app_cred only when the the command you are typing invokes one of these tools. - # Tip: Remove the next line to always show google_app_cred. - typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_SHOW_ON_COMMAND='terraform|pulumi' - - # Google application credentials classes for the purpose of using different colors, icons and - # expansions with different credentials. - # - # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES is an array with even number of elements. The first - # element in each pair defines a pattern against which the current kubernetes context gets - # matched. More specifically, it's P9K_CONTENT prior to the application of context expansion - # (see below) that gets matched. If you unset all POWERLEVEL9K_GOOGLE_APP_CRED_*CONTENT_EXPANSION - # parameters, you'll see this value in your prompt. The second element of each pair in - # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES defines the context class. Patterns are tried in order. - # The first match wins. - # - # For example, given these settings: - # - # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( - # '*:*prod*:*' PROD - # '*:*test*:*' TEST - # '*' DEFAULT) - # - # If your current Google application credentials is "service_account deathray-testing x@y.com", - # its class is TEST because it doesn't match the pattern '* *prod* *' but does match '* *test* *'. - # - # You can define different colors, icons and content expansions for different classes: - # - # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_FOREGROUND=28 - # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' - # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_CONTENT_EXPANSION='$P9K_GOOGLE_APP_CRED_PROJECT_ID' - typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( - # '*:*prod*:*' PROD # These values are examples that are unlikely - # '*:*test*:*' TEST # to match your needs. Customize them as needed. - '*' DEFAULT) - typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_FOREGROUND=32 - # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Use POWERLEVEL9K_GOOGLE_APP_CRED_CONTENT_EXPANSION to specify the content displayed by - # google_app_cred segment. Parameter expansions are very flexible and fast, too. See reference: - # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. - # - # You can use the following parameters in the expansion. Each of them corresponds to one of the - # fields in the JSON file pointed to by GOOGLE_APPLICATION_CREDENTIALS. - # - # Parameter | JSON key file field - # ---------------------------------+--------------- - # P9K_GOOGLE_APP_CRED_TYPE | type - # P9K_GOOGLE_APP_CRED_PROJECT_ID | project_id - # P9K_GOOGLE_APP_CRED_CLIENT_EMAIL | client_email - # - # Note: ${VARIABLE//\%/%%} expands to ${VARIABLE} with all occurrences of '%' replaced by '%%'. - typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_CONTENT_EXPANSION='${P9K_GOOGLE_APP_CRED_PROJECT_ID//\%/%%}' - - ###############################[ public_ip: public IP address ]############################### - # Public IP color. - typeset -g POWERLEVEL9K_PUBLIC_IP_FOREGROUND=94 - # Custom icon. - # typeset -g POWERLEVEL9K_PUBLIC_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ########################[ vpn_ip: virtual private network indicator ]######################### - # VPN IP color. - typeset -g POWERLEVEL9K_VPN_IP_FOREGROUND=81 - # When on VPN, show just an icon without the IP address. - # Tip: To display the private IP address when on VPN, remove the next line. - typeset -g POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION= - # Regular expression for the VPN network interface. Run `ifconfig` or `ip -4 a show` while on VPN - # to see the name of the interface. - typeset -g POWERLEVEL9K_VPN_IP_INTERFACE='(wg|(.*tun))[0-9]*' - # Custom icon. - # typeset -g POWERLEVEL9K_VPN_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ###########[ ip: ip address and bandwidth usage for a specified network interface ]########### - # IP color. - typeset -g POWERLEVEL9K_IP_FOREGROUND=38 - # The following parameters are accessible within the expansion: - # - # Parameter | Meaning - # ----------------------+--------------- - # P9K_IP_IP | IP address - # P9K_IP_INTERFACE | network interface - # P9K_IP_RX_BYTES | total number of bytes received - # P9K_IP_TX_BYTES | total number of bytes sent - # P9K_IP_RX_RATE | receive rate (since last prompt) - # P9K_IP_TX_RATE | send rate (since last prompt) - typeset -g POWERLEVEL9K_IP_CONTENT_EXPANSION='%70F⇣$P9K_IP_RX_RATE %215F⇡$P9K_IP_TX_RATE %38F$P9K_IP_IP' - # Show information for the first network interface whose name matches this regular expression. - # Run `ifconfig` or `ip -4 a show` to see the names of all network interfaces. - typeset -g POWERLEVEL9K_IP_INTERFACE='e.*' - # Custom icon. - # typeset -g POWERLEVEL9K_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' - - #########################[ proxy: system-wide http/https/ftp proxy ]########################## - # Proxy color. - typeset -g POWERLEVEL9K_PROXY_FOREGROUND=68 - # Custom icon. - # typeset -g POWERLEVEL9K_PROXY_VISUAL_IDENTIFIER_EXPANSION='⭐' - - ################################[ battery: internal battery ]################################# - # Show battery in red when it's below this level and not connected to power supply. - typeset -g POWERLEVEL9K_BATTERY_LOW_THRESHOLD=20 - typeset -g POWERLEVEL9K_BATTERY_LOW_FOREGROUND=160 - # Show battery in green when it's charging or fully charged. - typeset -g POWERLEVEL9K_BATTERY_{CHARGING,CHARGED}_FOREGROUND=70 - # Show battery in yellow when it's discharging. - typeset -g POWERLEVEL9K_BATTERY_DISCONNECTED_FOREGROUND=178 - # Battery pictograms going from low to high level of charge. - typeset -g POWERLEVEL9K_BATTERY_STAGES=('%K{232}▁' '%K{232}▂' '%K{232}▃' '%K{232}▄' '%K{232}▅' '%K{232}▆' '%K{232}▇' '%K{232}█') - # Don't show the remaining time to charge/discharge. - typeset -g POWERLEVEL9K_BATTERY_VERBOSE=false - - #####################################[ wifi: wifi speed ]##################################### - # WiFi color. - typeset -g POWERLEVEL9K_WIFI_FOREGROUND=68 - # Custom icon. - # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Use different colors and icons depending on signal strength ($P9K_WIFI_BARS). - # - # # Wifi colors and icons for different signal strength levels (low to high). - # typeset -g my_wifi_fg=(68 68 68 68 68) # <-- change these values - # typeset -g my_wifi_icon=('WiFi' 'WiFi' 'WiFi' 'WiFi' 'WiFi') # <-- change these values - # - # typeset -g POWERLEVEL9K_WIFI_CONTENT_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}$P9K_WIFI_LAST_TX_RATE Mbps' - # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}${my_wifi_icon[P9K_WIFI_BARS+1]}' - # - # The following parameters are accessible within the expansions: - # - # Parameter | Meaning - # ----------------------+--------------- - # P9K_WIFI_SSID | service set identifier, a.k.a. network name - # P9K_WIFI_LINK_AUTH | authentication protocol such as "wpa2-psk" or "none" - # P9K_WIFI_LAST_TX_RATE | wireless transmit rate in megabits per second - # P9K_WIFI_RSSI | signal strength in dBm, from -120 to 0 - # P9K_WIFI_NOISE | noise in dBm, from -120 to 0 - # P9K_WIFI_BARS | signal strength in bars, from 0 to 4 (derived from P9K_WIFI_RSSI and P9K_WIFI_NOISE) - # - # All parameters except P9K_WIFI_BARS are extracted from the output of the following command: - # - # /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I - - ####################################[ time: current time ]#################################### - # Current time color. - typeset -g POWERLEVEL9K_TIME_FOREGROUND=66 - # Format for the current time: 09:51:02. See `man 3 strftime`. - typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M:%S}' - # If set to true, time will update when you hit enter. This way prompts for the past - # commands will contain the start times of their commands as opposed to the default - # behavior where they contain the end times of their preceding commands. - typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false - # Custom icon. - typeset -g POWERLEVEL9K_TIME_VISUAL_IDENTIFIER_EXPANSION= - # Custom prefix. - typeset -g POWERLEVEL9K_TIME_PREFIX='%248Fat ' - - # Example of a user-defined prompt segment. Function prompt_example will be called on every - # prompt if `example` prompt segment is added to POWERLEVEL9K_LEFT_PROMPT_ELEMENTS or - # POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS. It displays an icon and orange text greeting the user. - # - # Type `p10k help segment` for documentation and a more sophisticated example. - function prompt_example() { - p10k segment -f 208 -i '⭐' -t 'hello, %n' - } - - # User-defined prompt segments may optionally provide an instant_prompt_* function. Its job - # is to generate the prompt segment for display in instant prompt. See - # https://github.com/romkatv/powerlevel10k/blob/master/README.md#instant-prompt. - # - # Powerlevel10k will call instant_prompt_* at the same time as the regular prompt_* function - # and will record all `p10k segment` calls it makes. When displaying instant prompt, Powerlevel10k - # will replay these calls without actually calling instant_prompt_*. It is imperative that - # instant_prompt_* always makes the same `p10k segment` calls regardless of environment. If this - # rule is not observed, the content of instant prompt will be incorrect. - # - # Usually, you should either not define instant_prompt_* or simply call prompt_* from it. If - # instant_prompt_* is not defined for a segment, the segment won't be shown in instant prompt. - function instant_prompt_example() { - # Since prompt_example always makes the same `p10k segment` calls, we can call it from - # instant_prompt_example. This will give us the same `example` prompt segment in the instant - # and regular prompts. - prompt_example - } - - # User-defined prompt segments can be customized the same way as built-in segments. - # typeset -g POWERLEVEL9K_EXAMPLE_FOREGROUND=208 - # typeset -g POWERLEVEL9K_EXAMPLE_VISUAL_IDENTIFIER_EXPANSION='⭐' - - # Transient prompt works similarly to the builtin transient_rprompt option. It trims down prompt - # when accepting a command line. Supported values: - # - # - off: Don't change prompt when accepting a command line. - # - always: Trim down prompt when accepting a command line. - # - same-dir: Trim down prompt when accepting a command line unless this is the first command - # typed after changing current working directory. - typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always - - # Instant prompt mode. - # - # - off: Disable instant prompt. Choose this if you've tried instant prompt and found - # it incompatible with your zsh configuration files. - # - quiet: Enable instant prompt and don't print warnings when detecting console output - # during zsh initialization. Choose this if you've read and understood - # https://github.com/romkatv/powerlevel10k/blob/master/README.md#instant-prompt. - # - verbose: Enable instant prompt and print a warning when detecting console output during - # zsh initialization. Choose this if you've never tried instant prompt, haven't - # seen the warning, or if you are unsure what this all means. - typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose - - # Hot reload allows you to change POWERLEVEL9K options after Powerlevel10k has been initialized. - # For example, you can type POWERLEVEL9K_BACKGROUND=red and see your prompt turn red. Hot reload - # can slow down prompt by 1-2 milliseconds, so it's better to keep it turned off unless you - # really need it. - typeset -g POWERLEVEL9K_DISABLE_HOT_RELOAD=true - - # If p10k is already loaded, reload configuration. - # This works even with POWERLEVEL9K_DISABLE_HOT_RELOAD=true. - (( ! $+functions[p10k] )) || p10k reload + # Default background color. + typeset -g POWERLEVEL9K_BACKGROUND=238 + + # Separator between same-color segments on the left. + typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR='%246F|' + # Separator between same-color segments on the right. + typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR='%246F|' + # Separator between different-color segments on the left. + typeset -g POWERLEVEL9K_LEFT_SEGMENT_SEPARATOR='' + # Separator between different-color segments on the right. + typeset -g POWERLEVEL9K_RIGHT_SEGMENT_SEPARATOR='' + # The right end of left prompt. + typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='▓▒░' + # The left end of right prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='░▒▓' + # The left end of left prompt. + typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL='' + # The right end of right prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL='' + # Left prompt terminator for lines without any segments. + typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= + + #################################[ os_icon: os identifier ]################################## + # OS identifier color. + typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND=255 + # Make the icon bold. + typeset -g POWERLEVEL9K_OS_ICON_CONTENT_EXPANSION='%B${P9K_CONTENT}' + + ################################[ prompt_char: prompt symbol ]################################ + # Transparent background. + typeset -g POWERLEVEL9K_PROMPT_CHAR_BACKGROUND= + # Green prompt symbol if the last command succeeded. + typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=76 + # Red prompt symbol if the last command failed. + typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=196 + # Default prompt symbol. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION='❯' + # Prompt symbol in command vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION='❮' + # Prompt symbol in visual vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='Ⅴ' + # Prompt symbol in overwrite vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION='▶' + typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true + # No line terminator if prompt_char is the last segment. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= + # No line introducer if prompt_char is the first segment. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL= + # No surrounding whitespace. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_{LEFT,RIGHT}_WHITESPACE= + + ##################################[ dir: current directory ]################################## + # Default current directory color. + typeset -g POWERLEVEL9K_DIR_FOREGROUND=31 + # If directory is too long, shorten some of its segments to the shortest possible unique + # prefix. The shortened directory can be tab-completed to the original. + typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique + # Replace removed segment suffixes with this symbol. + typeset -g POWERLEVEL9K_SHORTEN_DELIMITER= + # Color of the shortened directory segments. + typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=103 + # Color of the anchor directory segments. Anchor segments are never shortened. The first + # segment is always an anchor. + typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=39 + # Display anchor directory segments in bold. + typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=true + # Don't shorten directories that contain any of these files. They are anchors. + local anchor_files=( + .bzr + .citc + .git + .hg + .node-version + .python-version + .ruby-version + .shorten_folder_marker + .svn + .terraform + CVS + Cargo.toml + composer.json + go.mod + package.json + ) + typeset -g POWERLEVEL9K_SHORTEN_FOLDER_MARKER="(${(j:|:)anchor_files})" + # If set to true, remove everything before the last (deepest) subdirectory that contains files + # matching $POWERLEVEL9K_SHORTEN_FOLDER_MARKER. For example, when the current directory is + # /foo/bar/git_repo/baz, prompt will display git_repo/baz. This assumes that /foo/bar/git_repo + # contains a marker (.git) and other directories don't. + typeset -g POWERLEVEL9K_DIR_TRUNCATE_BEFORE_MARKER=false + # Don't shorten this many last directory segments. They are anchors. + typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=1 + # Shorten directory if it's longer than this even if there is space for it. The value can + # be either absolute (e.g., '80') or a percentage of terminal width (e.g, '50%'). If empty, + # directory will be shortened only when prompt doesn't fit or when other parameters demand it + # (see POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS and POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT below). + # If set to `0`, directory will always be shortened to its minimum length. + typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80 + # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least this + # many columns for typing commands. + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40 + # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least + # COLUMNS * POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT * 0.01 columns for typing commands. + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT=50 + # If set to true, embed a hyperlink into the directory. Useful for quickly + # opening a directory in the file manager simply by clicking the link. + # Can also be handy when the directory is shortened, as it allows you to see + # the full directory that was used in previous commands. + typeset -g POWERLEVEL9K_DIR_HYPERLINK=false + + # Enable special styling for non-writable directories. + typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=true + # Show this icon when the current directory is not writable. POWERLEVEL9K_DIR_SHOW_WRITABLE + # above must be set to true for this parameter to have effect. + typeset -g POWERLEVEL9K_DIR_NOT_WRITABLE_VISUAL_IDENTIFIER_EXPANSION='∅' + + # Custom prefix. + # typeset -g POWERLEVEL9K_DIR_PREFIX='%248Fin ' + + # POWERLEVEL9K_DIR_CLASSES allows you to specify custom icons for different directories. + # It must be an array with 3 * N elements. Each triplet consists of: + # + # 1. A pattern against which the current directory is matched. Matching is done with + # extended_glob option enabled. + # 2. Directory class for the purpose of styling. + # 3. Icon. + # + # Triplets are tried in order. The first triplet whose pattern matches $PWD wins. If there + # are no matches, the directory will have no icon. + # + # Example: + # + # typeset -g POWERLEVEL9K_DIR_CLASSES=( + # '~/work(|/*)' WORK '(╯°□°)╯︵ ┻━┻' + # '~(|/*)' HOME '⌂' + # '*' DEFAULT '') + # + # With these settings, the current directory in the prompt may look like this: + # + # (╯°□°)╯︵ ┻━┻ ~/work/projects/important/urgent + # + # Or like this: + # + # ⌂ ~/best/powerlevel10k + # + # You can also set different colors for directories of different classes. Remember to override + # FOREGROUND, SHORTENED_FOREGROUND and ANCHOR_FOREGROUND for every directory class that you wish + # to have its own color. + # + # typeset -g POWERLEVEL9K_DIR_WORK_FOREGROUND=31 + # typeset -g POWERLEVEL9K_DIR_WORK_SHORTENED_FOREGROUND=103 + # typeset -g POWERLEVEL9K_DIR_WORK_ANCHOR_FOREGROUND=39 + # + typeset -g POWERLEVEL9K_DIR_CLASSES=() + + #####################################[ vcs: git status ]###################################### + # Branch icon. Set this parameter to '\uF126 ' for the popular Powerline branch icon. + typeset -g POWERLEVEL9K_VCS_BRANCH_ICON= + POWERLEVEL9K_VCS_BRANCH_ICON=${(g::)POWERLEVEL9K_VCS_BRANCH_ICON} + + # Untracked files icon. It's really a question mark, your font isn't broken. + # Change the value of this parameter to show a different icon. + typeset -g POWERLEVEL9K_VCS_UNTRACKED_ICON='?' + POWERLEVEL9K_VCS_UNTRACKED_ICON=${(g::)POWERLEVEL9K_VCS_UNTRACKED_ICON} + + # Formatter for Git status. + # + # Example output: master ⇣42⇡42 *42 merge ~42 +42 !42 ?42. + # + # You can edit the function to customize how Git status looks. + # + # VCS_STATUS_* parameters are set by gitstatus plugin. See reference: + # https://github.com/romkatv/gitstatus/blob/master/gitstatus.plugin.zsh. + function my_git_formatter() { + emulate -L zsh + + if [[ -n $P9K_CONTENT ]]; then + # If P9K_CONTENT is not empty, use it. It's either "loading" or from vcs_info (not from + # gitstatus plugin). VCS_STATUS_* parameters are not available in this case. + typeset -g my_git_format=$P9K_CONTENT + return + fi + + if (($1)); then + # Styling for up-to-date Git status. + local meta='%248F' # grey foreground + local clean='%76F' # green foreground + local modified='%178F' # yellow foreground + local untracked='%39F' # blue foreground + local conflicted='%196F' # red foreground + else + # Styling for incomplete and stale Git status. + local meta='%244F' # grey foreground + local clean='%244F' # grey foreground + local modified='%244F' # grey foreground + local untracked='%244F' # grey foreground + local conflicted='%244F' # grey foreground + fi + + local res + local where # branch or tag + if [[ -n $VCS_STATUS_LOCAL_BRANCH ]]; then + res+="${clean}${POWERLEVEL9K_VCS_BRANCH_ICON}" + where=${(V)VCS_STATUS_LOCAL_BRANCH} + elif [[ -n $VCS_STATUS_TAG ]]; then + res+="${meta}#" + where=${(V)VCS_STATUS_TAG} + fi + + # If local branch name or tag is at most 32 characters long, show it in full. + # Otherwise show the first 12 … the last 12. + (($#where > 50)) && where[13,-13]="…" + res+="${clean}${where//\%/%%}" # escape % + + # Display the current Git commit if there is no branch or tag. + # Tip: To always display the current Git commit, remove `[[ -z $where ]] &&` from the next line. + [[ -z $where ]] && res+="${meta}@${clean}${VCS_STATUS_COMMIT[1,8]}" + + # Show tracking branch name if it differs from local branch. + if [[ -n ${VCS_STATUS_REMOTE_BRANCH:#$VCS_STATUS_LOCAL_BRANCH} ]]; then + res+="${meta}:${clean}${(V)VCS_STATUS_REMOTE_BRANCH//\%/%%}" # escape % + fi + + # ⇣42 if behind the remote. + ((VCS_STATUS_COMMITS_BEHIND)) && res+=" ${clean}⇣${VCS_STATUS_COMMITS_BEHIND}" + # ⇡42 if ahead of the remote; no leading space if also behind the remote: ⇣42⇡42. + ((VCS_STATUS_COMMITS_AHEAD && !VCS_STATUS_COMMITS_BEHIND)) && res+=" " + ((VCS_STATUS_COMMITS_AHEAD)) && res+="${clean}⇡${VCS_STATUS_COMMITS_AHEAD}" + # ⇠42 if behind the push remote. + ((VCS_STATUS_PUSH_COMMITS_BEHIND)) && res+=" ${clean}⇠${VCS_STATUS_PUSH_COMMITS_BEHIND}" + ((VCS_STATUS_PUSH_COMMITS_AHEAD && !VCS_STATUS_PUSH_COMMITS_BEHIND)) && res+=" " + # ⇢42 if ahead of the push remote; no leading space if also behind: ⇠42⇢42. + ((VCS_STATUS_PUSH_COMMITS_AHEAD)) && res+="${clean}⇢${VCS_STATUS_PUSH_COMMITS_AHEAD}" + # *42 if have stashes. + ((VCS_STATUS_STASHES)) && res+=" ${clean}*${VCS_STATUS_STASHES}" + # 'merge' if the repo is in an unusual state. + [[ -n $VCS_STATUS_ACTION ]] && res+=" ${conflicted}${VCS_STATUS_ACTION}" + # ~42 if have merge conflicts. + ((VCS_STATUS_NUM_CONFLICTED)) && res+=" ${conflicted}~${VCS_STATUS_NUM_CONFLICTED}" + # +42 if have staged changes. + ((VCS_STATUS_NUM_STAGED)) && res+=" ${modified}+${VCS_STATUS_NUM_STAGED}" + # !42 if have unstaged changes. + ((VCS_STATUS_NUM_UNSTAGED)) && res+=" ${modified}!${VCS_STATUS_NUM_UNSTAGED}" + # ?42 if have untracked files. It's really a question mark, your font isn't broken. + # See POWERLEVEL9K_VCS_UNTRACKED_ICON above if you want to use a different icon. + # Remove the next line if you don't want to see untracked files at all. + ((VCS_STATUS_NUM_UNTRACKED)) && res+=" ${untracked}${POWERLEVEL9K_VCS_UNTRACKED_ICON}${VCS_STATUS_NUM_UNTRACKED}" + # "─" if the number of unstaged files is unknown. This can happen due to + # POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY (see below) being set to a non-negative number lower + # than the number of files in the Git index, or due to bash.showDirtyState being set to false + # in the repository config. The number of staged and untracked files may also be unknown + # in this case. + ((VCS_STATUS_HAS_UNSTAGED == -1)) && res+=" ${modified}─" + + typeset -g my_git_format=$res + } + functions -M my_git_formatter 2>/dev/null + + # Don't count the number of unstaged, untracked and conflicted files in Git repositories with + # more than this many files in the index. Negative value means infinity. + # + # If you are working in Git repositories with tens of millions of files and seeing performance + # sagging, try setting POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY to a number lower than the output + # of `git ls-files | wc -l`. Alternatively, add `bash.showDirtyState = false` to the repository's + # config: `git config bash.showDirtyState false`. + typeset -g POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY=-1 + + # Don't show Git status in prompt for repositories whose workdir matches this pattern. + # For example, if set to '~', the Git repository at $HOME/.git will be ignored. + # Multiple patterns can be combined with '|': '~|~/some/dir'. + typeset -g POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN='~' + + # Disable the default Git status formatting. + typeset -g POWERLEVEL9K_VCS_DISABLE_GITSTATUS_FORMATTING=true + # Install our own Git status formatter. + typeset -g POWERLEVEL9K_VCS_CONTENT_EXPANSION='${$((my_git_formatter(1)))+${my_git_format}}' + typeset -g POWERLEVEL9K_VCS_LOADING_CONTENT_EXPANSION='${$((my_git_formatter(0)))+${my_git_format}}' + # Enable counters for staged, unstaged, etc. + typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1 + + # Icon color. + typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_COLOR=76 + typeset -g POWERLEVEL9K_VCS_LOADING_VISUAL_IDENTIFIER_COLOR=244 + # Custom icon. + typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_EXPANSION= + # Custom prefix. + typeset -g POWERLEVEL9K_VCS_PREFIX='%248Fon ' + + # Show status of repositories of these types. You can add svn and/or hg if you are + # using them. If you do, your prompt may become slow even when your current directory + # isn't in an svn or hg reposotiry. + typeset -g POWERLEVEL9K_VCS_BACKENDS=(git) + + # These settings are used for repositories other than Git or when gitstatusd fails and + # Powerlevel10k has to fall back to using vcs_info. + typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND=76 + typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND=76 + typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND=178 + + ##########################[ status: exit code of the last command ]########################### + # Enable OK_PIPE, ERROR_PIPE and ERROR_SIGNAL status states to allow us to enable, disable and + # style them independently from the regular OK and ERROR state. + typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true + + # Status on success. No content, just an icon. No need to show it if prompt_char is enabled as + # it will signify success by turning green. + typeset -g POWERLEVEL9K_STATUS_OK=true + typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND=70 + typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔' + + # Status when some part of a pipe command fails but the overall exit status is zero. It may look + # like this: 1|0. + typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND=70 + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔' + + # Status when it's just an error code (e.g., '1'). No need to show it if prompt_char is enabled as + # it will signify error by turning red. + typeset -g POWERLEVEL9K_STATUS_ERROR=true + typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND=160 + typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='х' + + # Status when the last command was terminated by a signal. + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND=160 + # Use terse signal names: "INT" instead of "SIGINT(2)". + typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='х' + + # Status when some part of a pipe command fails and the overall exit status is also non-zero. + # It may look like this: 1|0. + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND=160 + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='х' + + ###################[ command_execution_time: duration of the last command ]################### + # Show duration of the last command if takes longer than this many seconds. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3 + # Show this many fractional digits. Zero means round to seconds. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0 + # Execution time color. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=248 + # Duration format: 1d 2h 3m 4s. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s' + # Custom icon. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_VISUAL_IDENTIFIER_EXPANSION= + # Custom prefix. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PREFIX='%248Ftook ' + + #######################[ background_jobs: presence of background jobs ]####################### + # Don't show the number of background jobs. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false + # Background jobs color. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_FOREGROUND=37 + # Custom icon. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VISUAL_IDENTIFIER_EXPANSION='≡' + + #######################[ direnv: direnv status (https://direnv.net/) ]######################## + # Direnv color. + typeset -g POWERLEVEL9K_DIRENV_FOREGROUND=178 + # Custom icon. + # typeset -g POWERLEVEL9K_DIRENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ asdf: asdf version manager (https://github.com/asdf-vm/asdf) ]############### + # Default asdf color. Only used to display tools for which there is no color override (see below). + typeset -g POWERLEVEL9K_ASDF_FOREGROUND=66 + + # There are three parameters that can be used to hide tools. If at least one of them decides + # to hide a tool, that tool gets hidden. POWERLEVEL9K_ASDF_SHOW_SYSTEM=false hides "system". To + # see the difference between POWERLEVEL9K_ASDF_SOURCES and POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW + # consider the effect of the following commands: + # + # asdf local python 3.8.1 + # asdf global python 3.8.1 + # + # After running both commands the current python version is 3.8.1 and its source is "local" as + # it takes precedence over "global". If POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW is set to false, + # it'll hide python version in this case because 3.8.1 is the same as the global version. + # POWERLEVEL9K_ASDF_SOURCES will hide python version only if the value of this parameter doesn't + # contain "local". + + # Hide tool versions that don't come from one of these sources. + # + # Available sources: + # + # - shell `asdf current` says "set by ASDF_${TOOL}_VERSION environment variable" + # - local `asdf current` says "set by /some/not/home/directory/file" + # - global `asdf current` says "set by /home/username/file" + # + # Note: If this parameter is set to (shell local global), it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SOURCES. + typeset -g POWERLEVEL9K_ASDF_SOURCES=(shell local global) + + # If set to false, hide tool versions that are the same as global. + # + # Note: The name of this parameter doesn't reflect its meaning at all. + # Note: If this parameter is set to true, it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_PROMPT_ALWAYS_SHOW. + typeset -g POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW=false + + # If set to false, hide tool versions that are equal to "system". + # + # Note: If this parameter is set to true, it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SHOW_SYSTEM. + typeset -g POWERLEVEL9K_ASDF_SHOW_SYSTEM=true + + # Ruby version from asdf. + typeset -g POWERLEVEL9K_ASDF_RUBY_FOREGROUND=168 + # typeset -g POWERLEVEL9K_ASDF_RUBY_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Python version from asdf. + typeset -g POWERLEVEL9K_ASDF_PYTHON_FOREGROUND=37 + # typeset -g POWERLEVEL9K_ASDF_PYTHON_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Go version from asdf. + typeset -g POWERLEVEL9K_ASDF_GO_FOREGROUND=37 + # typeset -g POWERLEVEL9K_ASDF_GO_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Node.js version from asdf. + typeset -g POWERLEVEL9K_ASDF_NODEJS_FOREGROUND=70 + # typeset -g POWERLEVEL9K_ASDF_NODEJS_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Rust version from asdf. + typeset -g POWERLEVEL9K_ASDF_RUST_FOREGROUND=37 + # typeset -g POWERLEVEL9K_ASDF_RUST_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # .NET Core version from asdf. + typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_FOREGROUND=134 + # typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Flutter version from asdf. + typeset -g POWERLEVEL9K_ASDF_FLUTTER_FOREGROUND=38 + # typeset -g POWERLEVEL9K_ASDF_FLUTTER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Lua version from asdf. + typeset -g POWERLEVEL9K_ASDF_LUA_FOREGROUND=32 + # typeset -g POWERLEVEL9K_ASDF_LUA_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Java version from asdf. + typeset -g POWERLEVEL9K_ASDF_JAVA_FOREGROUND=32 + # typeset -g POWERLEVEL9K_ASDF_JAVA_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Perl version from asdf. + typeset -g POWERLEVEL9K_ASDF_PERL_FOREGROUND=67 + # typeset -g POWERLEVEL9K_ASDF_PERL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Erlang version from asdf. + typeset -g POWERLEVEL9K_ASDF_ERLANG_FOREGROUND=125 + # typeset -g POWERLEVEL9K_ASDF_ERLANG_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Elixir version from asdf. + typeset -g POWERLEVEL9K_ASDF_ELIXIR_FOREGROUND=129 + # typeset -g POWERLEVEL9K_ASDF_ELIXIR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Postgres version from asdf. + typeset -g POWERLEVEL9K_ASDF_POSTGRES_FOREGROUND=31 + # typeset -g POWERLEVEL9K_ASDF_POSTGRES_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ nordvpn: nordvpn connection status, linux only (https://nordvpn.com/) ]########### + # NordVPN connection indicator color. + typeset -g POWERLEVEL9K_NORDVPN_FOREGROUND=39 + # Hide NordVPN connection indicator when not connected. + typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_CONTENT_EXPANSION= + typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_VISUAL_IDENTIFIER_EXPANSION= + # Custom icon. + # typeset -g POWERLEVEL9K_NORDVPN_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #################[ ranger: ranger shell (https://github.com/ranger/ranger) ]################## + # Ranger shell color. + typeset -g POWERLEVEL9K_RANGER_FOREGROUND=178 + # Custom icon. + typeset -g POWERLEVEL9K_RANGER_VISUAL_IDENTIFIER_EXPANSION='▲' + + ######################[ nnn: nnn shell (https://github.com/jarun/nnn) ]####################### + # Nnn shell color. + typeset -g POWERLEVEL9K_NNN_FOREGROUND=72 + # Custom icon. + # typeset -g POWERLEVEL9K_NNN_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########################[ vim_shell: vim shell indicator (:sh) ]########################### + # Vim shell indicator color. + typeset -g POWERLEVEL9K_VIM_SHELL_FOREGROUND=34 + # Custom icon. + # typeset -g POWERLEVEL9K_VIM_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######[ midnight_commander: midnight commander shell (https://midnight-commander.org/) ]###### + # Midnight Commander shell color. + typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_FOREGROUND=178 + # Custom icon. + # typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ nix_shell: nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) ]## + # Nix shell color. + typeset -g POWERLEVEL9K_NIX_SHELL_FOREGROUND=74 + + # Tip: If you want to see just the icon without "pure" and "impure", uncomment the next line. + # typeset -g POWERLEVEL9K_NIX_SHELL_CONTENT_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_NIX_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################################[ disk_usgae: disk usage ]################################## + # Colors for different levels of disk usage. + typeset -g POWERLEVEL9K_DISK_USAGE_NORMAL_FOREGROUND=35 + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_FOREGROUND=220 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_FOREGROUND=160 + # Thresholds for different levels of disk usage (percentage points). + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL=90 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_LEVEL=95 + # If set to true, hide disk usage when below $POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL percent. + typeset -g POWERLEVEL9K_DISK_USAGE_ONLY_WARNING=false + # Custom icon. + # typeset -g POWERLEVEL9K_DISK_USAGE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ vi_mode: vi mode (you don't need this if you've enabled prompt_char) ]########### + # Text and color for normal (a.k.a. command) vi mode. + typeset -g POWERLEVEL9K_VI_COMMAND_MODE_STRING=NORMAL + typeset -g POWERLEVEL9K_VI_MODE_NORMAL_FOREGROUND=106 + # Text and color for visual vi mode. + typeset -g POWERLEVEL9K_VI_VISUAL_MODE_STRING=VISUAL + typeset -g POWERLEVEL9K_VI_MODE_VISUAL_FOREGROUND=68 + # Text and color for overtype (a.k.a. overwrite and replace) vi mode. + typeset -g POWERLEVEL9K_VI_OVERWRITE_MODE_STRING=OVERTYPE + typeset -g POWERLEVEL9K_VI_MODE_OVERWRITE_FOREGROUND=172 + # Text and color for insert vi mode. + typeset -g POWERLEVEL9K_VI_INSERT_MODE_STRING= + typeset -g POWERLEVEL9K_VI_MODE_INSERT_FOREGROUND=66 + + # Custom icon. + typeset -g POWERLEVEL9K_RANGER_VISUAL_IDENTIFIER_EXPANSION='▲' + + ######################################[ ram: free RAM ]####################################### + # RAM color. + typeset -g POWERLEVEL9K_RAM_FOREGROUND=66 + # Custom icon. + # typeset -g POWERLEVEL9K_RAM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################################[ swap: used swap ]###################################### + # Swap color. + typeset -g POWERLEVEL9K_SWAP_FOREGROUND=96 + # Custom icon. + # typeset -g POWERLEVEL9K_SWAP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################################[ load: CPU load ]###################################### + # Show average CPU load over this many last minutes. Valid values are 1, 5 and 15. + typeset -g POWERLEVEL9K_LOAD_WHICH=5 + # Load color when load is under 50%. + typeset -g POWERLEVEL9K_LOAD_NORMAL_FOREGROUND=66 + # Load color when load is between 50% and 70%. + typeset -g POWERLEVEL9K_LOAD_WARNING_FOREGROUND=178 + # Load color when load is over 70%. + typeset -g POWERLEVEL9K_LOAD_CRITICAL_FOREGROUND=166 + # Custom icon. + # typeset -g POWERLEVEL9K_LOAD_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ todo: todo items (https://github.com/todotxt/todo.txt-cli) ]################ + # Todo color. + typeset -g POWERLEVEL9K_TODO_FOREGROUND=110 + # Hide todo when the total number of tasks is zero. + typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_TOTAL=true + # Hide todo when the number of tasks after filtering is zero. + typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_FILTERED=false + + # Todo format. The following parameters are available within the expansion. + # + # - P9K_TODO_TOTAL_TASK_COUNT The total number of tasks. + # - P9K_TODO_FILTERED_TASK_COUNT The number of tasks after filtering. + # + # These variables correspond to the last line of the output of `todo.sh -p ls`: + # + # TODO: 24 of 42 tasks shown + # + # Here 24 is P9K_TODO_FILTERED_TASK_COUNT and 42 is P9K_TODO_TOTAL_TASK_COUNT. + # + # typeset -g POWERLEVEL9K_TODO_CONTENT_EXPANSION='$P9K_TODO_FILTERED_TASK_COUNT' + + # Custom icon. + # typeset -g POWERLEVEL9K_TODO_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ timewarrior: timewarrior tracking status (https://timewarrior.net/) ]############ + # Timewarrior color. + typeset -g POWERLEVEL9K_TIMEWARRIOR_FOREGROUND=110 + # If the tracked task is longer than 24 characters, truncate and append "…". + # Tip: To always display tasks without truncation, delete the following parameter. + # Tip: To hide task names and display just the icon when time tracking is enabled, set the + # value of the following parameter to "". + typeset -g POWERLEVEL9K_TIMEWARRIOR_CONTENT_EXPANSION='${P9K_CONTENT:0:24}${${P9K_CONTENT:24}:+…}' + + # Custom icon. + # typeset -g POWERLEVEL9K_TIMEWARRIOR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################################[ context: user@hostname ]################################## + # Context color when running with privileges. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=178 + # Context color in SSH without privileges. + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_FOREGROUND=180 + # Default context color (no privileges, no SSH). + typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=180 + + # Context format when running with privileges: bold user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%B%n@%m' + # Context format when in SSH without privileges: user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_TEMPLATE='%n@%m' + # Default context format (no privileges, no SSH): user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE='%n@%m' + + # Don't show context unless running with privileges or in SSH. + # Tip: Remove the next line to always show context. + typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_{CONTENT,VISUAL_IDENTIFIER}_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_CONTEXT_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + typeset -g POWERLEVEL9K_CONTEXT_PREFIX='%248Fwith ' + + ###[ virtualenv: python virtual environment (https://docs.python.org/3/library/venv.html) ]### + # Python virtual environment color. + typeset -g POWERLEVEL9K_VIRTUALENV_FOREGROUND=37 + # Don't show Python version next to the virtual environment name. + typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_PYTHON_VERSION=false + # Separate environment name from Python version only with a space. + typeset -g POWERLEVEL9K_VIRTUALENV_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + # typeset -g POWERLEVEL9K_VIRTUALENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################[ anaconda: conda environment (https://conda.io/) ]###################### + # Anaconda environment color. + typeset -g POWERLEVEL9K_ANACONDA_FOREGROUND=37 + # Don't show Python version next to the anaconda environment name. + typeset -g POWERLEVEL9K_ANACONDA_SHOW_PYTHON_VERSION=false + # Separate environment name from Python version only with a space. + typeset -g POWERLEVEL9K_ANACONDA_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + # typeset -g POWERLEVEL9K_ANACONDA_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ pyenv: python environment (https://github.com/pyenv/pyenv) ]################ + # Pyenv color. + typeset -g POWERLEVEL9K_PYENV_FOREGROUND=37 + # Hide python version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PYENV_SOURCES=(shell local global) + # If set to false, hide python version if it's the same as global: + # $(pyenv version-name) == $(pyenv global). + typeset -g POWERLEVEL9K_PYENV_PROMPT_ALWAYS_SHOW=false + # Custom icon. + # typeset -g POWERLEVEL9K_PYENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ goenv: go environment (https://github.com/syndbg/goenv) ]################ + # Goenv color. + typeset -g POWERLEVEL9K_GOENV_FOREGROUND=37 + # Hide go version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_GOENV_SOURCES=(shell local global) + # If set to false, hide go version if it's the same as global: + # $(goenv version-name) == $(goenv global). + typeset -g POWERLEVEL9K_GOENV_PROMPT_ALWAYS_SHOW=false + # Custom icon. + # typeset -g POWERLEVEL9K_GOENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ nodenv: node.js version from nodenv (https://github.com/nodenv/nodenv) ]########## + # Nodenv color. + typeset -g POWERLEVEL9K_NODENV_FOREGROUND=70 + # Don't show node version if it's the same as global: $(nodenv version-name) == $(nodenv global). + typeset -g POWERLEVEL9K_NODENV_PROMPT_ALWAYS_SHOW=false + # Custom icon. + # typeset -g POWERLEVEL9K_NODENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############[ nvm: node.js version from nvm (https://github.com/nvm-sh/nvm) ]############### + # Nvm color. + typeset -g POWERLEVEL9K_NVM_FOREGROUND=70 + # Custom icon. + # typeset -g POWERLEVEL9K_NVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ############[ nodeenv: node.js environment (https://github.com/ekalinin/nodeenv) ]############ + # Nodeenv color. + typeset -g POWERLEVEL9K_NODEENV_FOREGROUND=70 + # Don't show Node version next to the environment name. + typeset -g POWERLEVEL9K_NODEENV_SHOW_NODE_VERSION=false + # Separate environment name from Node version only with a space. + typeset -g POWERLEVEL9K_NODEENV_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + # typeset -g POWERLEVEL9K_NODEENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############################[ node_version: node.js version ]############################### + # Node version color. + typeset -g POWERLEVEL9K_NODE_VERSION_FOREGROUND=70 + # Show node version only when in a directory tree containing package.json. + typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_NODE_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ go_version: go version (https://golang.org) ]######################## + # Go version color. + typeset -g POWERLEVEL9K_GO_VERSION_FOREGROUND=37 + # Show go version only when in a go project subdirectory. + typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_GO_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #################[ rust_version: rustc version (https://www.rust-lang.org) ]################## + # Rust version color. + typeset -g POWERLEVEL9K_RUST_VERSION_FOREGROUND=37 + # Show rust version only when in a rust project subdirectory. + typeset -g POWERLEVEL9K_RUST_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_RUST_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ dotnet_version: .NET version (https://dotnet.microsoft.com) ]################ + # .NET version color. + typeset -g POWERLEVEL9K_DOTNET_VERSION_FOREGROUND=134 + # Show .NET version only when in a .NET project subdirectory. + typeset -g POWERLEVEL9K_DOTNET_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_DOTNET_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ rbenv: ruby version from rbenv (https://github.com/rbenv/rbenv) ]############## + # Rbenv color. + typeset -g POWERLEVEL9K_RBENV_FOREGROUND=168 + # Hide ruby version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_RBENV_SOURCES=(shell local global) + # If set to false, hide ruby version if it's the same as global: + # $(rbenv version-name) == $(rbenv global). + typeset -g POWERLEVEL9K_RBENV_PROMPT_ALWAYS_SHOW=false + # Custom icon. + # typeset -g POWERLEVEL9K_RBENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ rvm: ruby version from rvm (https://rvm.io) ]######################## + # Rvm color. + typeset -g POWERLEVEL9K_RVM_FOREGROUND=168 + # Don't show @gemset at the end. + typeset -g POWERLEVEL9K_RVM_SHOW_GEMSET=false + # Don't show ruby- at the front. + typeset -g POWERLEVEL9K_RVM_SHOW_PREFIX=false + # Custom icon. + # typeset -g POWERLEVEL9K_RVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ fvm: flutter version management (https://github.com/leoafarias/fvm) ]############ + # Fvm color. + typeset -g POWERLEVEL9K_FVM_FOREGROUND=38 + # Custom icon. + # typeset -g POWERLEVEL9K_FVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ luaenv: lua version from luaenv (https://github.com/cehoffman/luaenv) ]########### + # Lua color. + typeset -g POWERLEVEL9K_LUAENV_FOREGROUND=32 + # Hide lua version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_LUAENV_SOURCES=(shell local global) + # If set to false, hide lua version if it's the same as global: + # $(luaenv version-name) == $(luaenv global). + typeset -g POWERLEVEL9K_LUAENV_PROMPT_ALWAYS_SHOW=false + # Custom icon. + # typeset -g POWERLEVEL9K_LUAENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ jenv: java version from jenv (https://github.com/jenv/jenv) ]################ + # Java color. + typeset -g POWERLEVEL9K_JENV_FOREGROUND=32 + # Hide java version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_JENV_SOURCES=(shell local global) + # If set to false, hide java version if it's the same as global: + # $(jenv version-name) == $(jenv global). + typeset -g POWERLEVEL9K_JENV_PROMPT_ALWAYS_SHOW=false + # Custom icon. + # typeset -g POWERLEVEL9K_JENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ plenv: perl version from plenv (https://github.com/tokuhirom/plenv) ]############ + # Perl color. + typeset -g POWERLEVEL9K_PLENV_FOREGROUND=67 + # Hide perl version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PLENV_SOURCES=(shell local global) + # If set to false, hide perl version if it's the same as global: + # $(plenv version-name) == $(plenv global). + typeset -g POWERLEVEL9K_PLENV_PROMPT_ALWAYS_SHOW=false + # Custom icon. + # typeset -g POWERLEVEL9K_PLENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ terraform: terraform workspace (https://www.terraform.io) ]################# + # POWERLEVEL9K_TERRAFORM_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current terraform workspace gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_TERRAFORM_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_TERRAFORM_CLASSES defines the workspace class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current terraform workspace is "project_test", its class is TEST because "project_test" + # doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_TERRAFORM_DEFAULT_FOREGROUND=38 + # typeset -g POWERLEVEL9K_TERRAFORM_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ kubecontext: current kubernetes context (https://kubernetes.io/) ]############# + # Show kubecontext only when the the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show kubecontext. + typeset -g POWERLEVEL9K_KUBECONTEXT_SHOW_ON_COMMAND='kubectl|helm|kubens|kubectx|oc' + + # Kubernetes context classes for the purpose of using different colors, icons and expansions with + # different contexts. + # + # POWERLEVEL9K_KUBECONTEXT_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current kubernetes context gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_KUBECONTEXT_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_KUBECONTEXT_CLASSES defines the context class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current kubernetes context is "deathray-testing/default", its class is TEST + # because "deathray-testing/default" doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_FOREGROUND=134 + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='○' + + # Use POWERLEVEL9K_KUBECONTEXT_CONTENT_EXPANSION to specify the content displayed by kubecontext + # segment. Parameter expansions are very flexible and fast, too. See reference: + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. + # + # Within the expansion the following parameters are always available: + # + # - P9K_CONTENT The content that would've been displayed if there was no content + # expansion defined. + # - P9K_KUBECONTEXT_NAME The current context's name. Corresponds to column NAME in the + # output of `kubectl config get-contexts`. + # - P9K_KUBECONTEXT_CLUSTER The current context's cluster. Corresponds to column CLUSTER in the + # output of `kubectl config get-contexts`. + # - P9K_KUBECONTEXT_NAMESPACE The current context's namespace. Corresponds to column NAMESPACE + # in the output of `kubectl config get-contexts`. If there is no + # namespace, the parameter is set to "default". + # - P9K_KUBECONTEXT_USER The current context's user. Corresponds to column AUTHINFO in the + # output of `kubectl config get-contexts`. + # + # If the context points to Google Kubernetes Engine (GKE) or Elastic Kubernetes Service (EKS), + # the following extra parameters are available: + # + # - P9K_KUBECONTEXT_CLOUD_NAME Either "gke" or "eks". + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT Account/project ID. + # - P9K_KUBECONTEXT_CLOUD_ZONE Availability zone. + # - P9K_KUBECONTEXT_CLOUD_CLUSTER Cluster. + # + # P9K_KUBECONTEXT_CLOUD_* parameters are derived from P9K_KUBECONTEXT_CLUSTER. For example, + # if P9K_KUBECONTEXT_CLUSTER is "gke_my-account_us-east1-a_my-cluster-01": + # + # - P9K_KUBECONTEXT_CLOUD_NAME=gke + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=my-account + # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east1-a + # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 + # + # If P9K_KUBECONTEXT_CLUSTER is "arn:aws:eks:us-east-1:123456789012:cluster/my-cluster-01": + # + # - P9K_KUBECONTEXT_CLOUD_NAME=eks + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=123456789012 + # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east-1 + # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION= + # Show P9K_KUBECONTEXT_CLOUD_CLUSTER if it's not empty and fall back to P9K_KUBECONTEXT_NAME. + POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${P9K_KUBECONTEXT_CLOUD_CLUSTER:-${P9K_KUBECONTEXT_NAME}}' + # Append the current context's namespace if it's not "default". + POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${${:-/$P9K_KUBECONTEXT_NAMESPACE}:#/default}' + + # Custom prefix. + typeset -g POWERLEVEL9K_KUBECONTEXT_PREFIX='%248Fat ' + + #[ aws: aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) ]# + # Show aws only when the the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show aws. + typeset -g POWERLEVEL9K_AWS_SHOW_ON_COMMAND='aws|awless|terraform|pulumi' + + # POWERLEVEL9K_AWS_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current AWS profile gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_AWS_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_AWS_CLASSES defines the profile class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_AWS_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current AWS profile is "company_test", its class is TEST + # because "company_test" doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_AWS_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_AWS_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_AWS_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_AWS_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_AWS_DEFAULT_FOREGROUND=208 + # typeset -g POWERLEVEL9K_AWS_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ aws_eb_env: aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) ]# + # AWS Elastic Beanstalk environment color. + typeset -g POWERLEVEL9K_AWS_EB_ENV_FOREGROUND=70 + # Custom icon. + typeset -g POWERLEVEL9K_AWS_EB_ENV_VISUAL_IDENTIFIER_EXPANSION='eb' + + ##########[ azure: azure account name (https://docs.microsoft.com/en-us/cli/azure) ]########## + # Show azure only when the the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show azure. + typeset -g POWERLEVEL9K_AZURE_SHOW_ON_COMMAND='az|terraform|pulumi' + # Azure account name color. + typeset -g POWERLEVEL9K_AZURE_FOREGROUND=32 + # Custom icon. + typeset -g POWERLEVEL9K_AZURE_VISUAL_IDENTIFIER_EXPANSION='az' + + ##########[ gcloud: google cloud account and project (https://cloud.google.com/) ]########### + # Show gcloud only when the the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show gcloud. + typeset -g POWERLEVEL9K_GCLOUD_SHOW_ON_COMMAND='gcloud|gcs' + # Google cloud color. + typeset -g POWERLEVEL9K_GCLOUD_FOREGROUND=32 + + # Google cloud format. Change the value of POWERLEVEL9K_GCLOUD_CONTENT_EXPANSION if the default + # is too verbose or not informative enough. + # + # P9K_GCLOUD_ACCOUNT: the output of `gcloud config get-value account` + # P9K_GCLOUD_PROJECT: the output of `gcloud config get-value project` + # ${VARIABLE//\%/%%}: ${VARIABLE} with all occurrences of '%' replaced with '%%'. + # + typeset -g POWERLEVEL9K_GCLOUD_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT//\%/%%}' + + # Custom icon. + # typeset -g POWERLEVEL9K_GCLOUD_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ google_app_cred: google application credentials (https://cloud.google.com/docs/authentication/production) ]# + # Show google_app_cred only when the the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show google_app_cred. + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_SHOW_ON_COMMAND='terraform|pulumi' + + # Google application credentials classes for the purpose of using different colors, icons and + # expansions with different credentials. + # + # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES is an array with even number of elements. The first + # element in each pair defines a pattern against which the current kubernetes context gets + # matched. More specifically, it's P9K_CONTENT prior to the application of context expansion + # (see below) that gets matched. If you unset all POWERLEVEL9K_GOOGLE_APP_CRED_*CONTENT_EXPANSION + # parameters, you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES defines the context class. Patterns are tried in order. + # The first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( + # '*:*prod*:*' PROD + # '*:*test*:*' TEST + # '*' DEFAULT) + # + # If your current Google application credentials is "service_account deathray-testing x@y.com", + # its class is TEST because it doesn't match the pattern '* *prod* *' but does match '* *test* *'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_CONTENT_EXPANSION='$P9K_GOOGLE_APP_CRED_PROJECT_ID' + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( + # '*:*prod*:*' PROD # These values are examples that are unlikely + # '*:*test*:*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_FOREGROUND=32 + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use POWERLEVEL9K_GOOGLE_APP_CRED_CONTENT_EXPANSION to specify the content displayed by + # google_app_cred segment. Parameter expansions are very flexible and fast, too. See reference: + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. + # + # You can use the following parameters in the expansion. Each of them corresponds to one of the + # fields in the JSON file pointed to by GOOGLE_APPLICATION_CREDENTIALS. + # + # Parameter | JSON key file field + # ---------------------------------+--------------- + # P9K_GOOGLE_APP_CRED_TYPE | type + # P9K_GOOGLE_APP_CRED_PROJECT_ID | project_id + # P9K_GOOGLE_APP_CRED_CLIENT_EMAIL | client_email + # + # Note: ${VARIABLE//\%/%%} expands to ${VARIABLE} with all occurrences of '%' replaced by '%%'. + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_CONTENT_EXPANSION='${P9K_GOOGLE_APP_CRED_PROJECT_ID//\%/%%}' + + ###############################[ public_ip: public IP address ]############################### + # Public IP color. + typeset -g POWERLEVEL9K_PUBLIC_IP_FOREGROUND=94 + # Custom icon. + # typeset -g POWERLEVEL9K_PUBLIC_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ########################[ vpn_ip: virtual private network indicator ]######################### + # VPN IP color. + typeset -g POWERLEVEL9K_VPN_IP_FOREGROUND=81 + # When on VPN, show just an icon without the IP address. + # Tip: To display the private IP address when on VPN, remove the next line. + typeset -g POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION= + # Regular expression for the VPN network interface. Run `ifconfig` or `ip -4 a show` while on VPN + # to see the name of the interface. + typeset -g POWERLEVEL9K_VPN_IP_INTERFACE='(wg|(.*tun))[0-9]*' + # Custom icon. + # typeset -g POWERLEVEL9K_VPN_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ ip: ip address and bandwidth usage for a specified network interface ]########### + # IP color. + typeset -g POWERLEVEL9K_IP_FOREGROUND=38 + # The following parameters are accessible within the expansion: + # + # Parameter | Meaning + # ----------------------+--------------- + # P9K_IP_IP | IP address + # P9K_IP_INTERFACE | network interface + # P9K_IP_RX_BYTES | total number of bytes received + # P9K_IP_TX_BYTES | total number of bytes sent + # P9K_IP_RX_RATE | receive rate (since last prompt) + # P9K_IP_TX_RATE | send rate (since last prompt) + typeset -g POWERLEVEL9K_IP_CONTENT_EXPANSION='%70F⇣$P9K_IP_RX_RATE %215F⇡$P9K_IP_TX_RATE %38F$P9K_IP_IP' + # Show information for the first network interface whose name matches this regular expression. + # Run `ifconfig` or `ip -4 a show` to see the names of all network interfaces. + typeset -g POWERLEVEL9K_IP_INTERFACE='e.*' + # Custom icon. + # typeset -g POWERLEVEL9K_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #########################[ proxy: system-wide http/https/ftp proxy ]########################## + # Proxy color. + typeset -g POWERLEVEL9K_PROXY_FOREGROUND=68 + # Custom icon. + # typeset -g POWERLEVEL9K_PROXY_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################################[ battery: internal battery ]################################# + # Show battery in red when it's below this level and not connected to power supply. + typeset -g POWERLEVEL9K_BATTERY_LOW_THRESHOLD=20 + typeset -g POWERLEVEL9K_BATTERY_LOW_FOREGROUND=160 + # Show battery in green when it's charging or fully charged. + typeset -g POWERLEVEL9K_BATTERY_{CHARGING,CHARGED}_FOREGROUND=70 + # Show battery in yellow when it's discharging. + typeset -g POWERLEVEL9K_BATTERY_DISCONNECTED_FOREGROUND=178 + # Battery pictograms going from low to high level of charge. + typeset -g POWERLEVEL9K_BATTERY_STAGES=('%K{232}▁' '%K{232}▂' '%K{232}▃' '%K{232}▄' '%K{232}▅' '%K{232}▆' '%K{232}▇' '%K{232}█') + # Don't show the remaining time to charge/discharge. + typeset -g POWERLEVEL9K_BATTERY_VERBOSE=false + + #####################################[ wifi: wifi speed ]##################################### + # WiFi color. + typeset -g POWERLEVEL9K_WIFI_FOREGROUND=68 + # Custom icon. + # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use different colors and icons depending on signal strength ($P9K_WIFI_BARS). + # + # # Wifi colors and icons for different signal strength levels (low to high). + # typeset -g my_wifi_fg=(68 68 68 68 68) # <-- change these values + # typeset -g my_wifi_icon=('WiFi' 'WiFi' 'WiFi' 'WiFi' 'WiFi') # <-- change these values + # + # typeset -g POWERLEVEL9K_WIFI_CONTENT_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}$P9K_WIFI_LAST_TX_RATE Mbps' + # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}${my_wifi_icon[P9K_WIFI_BARS+1]}' + # + # The following parameters are accessible within the expansions: + # + # Parameter | Meaning + # ----------------------+--------------- + # P9K_WIFI_SSID | service set identifier, a.k.a. network name + # P9K_WIFI_LINK_AUTH | authentication protocol such as "wpa2-psk" or "none" + # P9K_WIFI_LAST_TX_RATE | wireless transmit rate in megabits per second + # P9K_WIFI_RSSI | signal strength in dBm, from -120 to 0 + # P9K_WIFI_NOISE | noise in dBm, from -120 to 0 + # P9K_WIFI_BARS | signal strength in bars, from 0 to 4 (derived from P9K_WIFI_RSSI and P9K_WIFI_NOISE) + # + # All parameters except P9K_WIFI_BARS are extracted from the output of the following command: + # + # /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I + + ####################################[ time: current time ]#################################### + # Current time color. + typeset -g POWERLEVEL9K_TIME_FOREGROUND=66 + # Format for the current time: 09:51:02. See `man 3 strftime`. + typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M:%S}' + # If set to true, time will update when you hit enter. This way prompts for the past + # commands will contain the start times of their commands as opposed to the default + # behavior where they contain the end times of their preceding commands. + typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false + # Custom icon. + typeset -g POWERLEVEL9K_TIME_VISUAL_IDENTIFIER_EXPANSION= + # Custom prefix. + typeset -g POWERLEVEL9K_TIME_PREFIX='%248Fat ' + + # Example of a user-defined prompt segment. Function prompt_example will be called on every + # prompt if `example` prompt segment is added to POWERLEVEL9K_LEFT_PROMPT_ELEMENTS or + # POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS. It displays an icon and orange text greeting the user. + # + # Type `p10k help segment` for documentation and a more sophisticated example. + function prompt_example() { + p10k segment -f 208 -i '⭐' -t 'hello, %n' + } + + # User-defined prompt segments may optionally provide an instant_prompt_* function. Its job + # is to generate the prompt segment for display in instant prompt. See + # https://github.com/romkatv/powerlevel10k/blob/master/README.md#instant-prompt. + # + # Powerlevel10k will call instant_prompt_* at the same time as the regular prompt_* function + # and will record all `p10k segment` calls it makes. When displaying instant prompt, Powerlevel10k + # will replay these calls without actually calling instant_prompt_*. It is imperative that + # instant_prompt_* always makes the same `p10k segment` calls regardless of environment. If this + # rule is not observed, the content of instant prompt will be incorrect. + # + # Usually, you should either not define instant_prompt_* or simply call prompt_* from it. If + # instant_prompt_* is not defined for a segment, the segment won't be shown in instant prompt. + function instant_prompt_example() { + # Since prompt_example always makes the same `p10k segment` calls, we can call it from + # instant_prompt_example. This will give us the same `example` prompt segment in the instant + # and regular prompts. + prompt_example + } + + # User-defined prompt segments can be customized the same way as built-in segments. + # typeset -g POWERLEVEL9K_EXAMPLE_FOREGROUND=208 + # typeset -g POWERLEVEL9K_EXAMPLE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Transient prompt works similarly to the builtin transient_rprompt option. It trims down prompt + # when accepting a command line. Supported values: + # + # - off: Don't change prompt when accepting a command line. + # - always: Trim down prompt when accepting a command line. + # - same-dir: Trim down prompt when accepting a command line unless this is the first command + # typed after changing current working directory. + typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always + + # Instant prompt mode. + # + # - off: Disable instant prompt. Choose this if you've tried instant prompt and found + # it incompatible with your zsh configuration files. + # - quiet: Enable instant prompt and don't print warnings when detecting console output + # during zsh initialization. Choose this if you've read and understood + # https://github.com/romkatv/powerlevel10k/blob/master/README.md#instant-prompt. + # - verbose: Enable instant prompt and print a warning when detecting console output during + # zsh initialization. Choose this if you've never tried instant prompt, haven't + # seen the warning, or if you are unsure what this all means. + typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose + + # Hot reload allows you to change POWERLEVEL9K options after Powerlevel10k has been initialized. + # For example, you can type POWERLEVEL9K_BACKGROUND=red and see your prompt turn red. Hot reload + # can slow down prompt by 1-2 milliseconds, so it's better to keep it turned off unless you + # really need it. + typeset -g POWERLEVEL9K_DISABLE_HOT_RELOAD=true + + # If p10k is already loaded, reload configuration. + # This works even with POWERLEVEL9K_DISABLE_HOT_RELOAD=true. + ((!$+functions[p10k])) || p10k reload } -(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]} +((${#p10k_config_opts})) && setopt ${p10k_config_opts[@]} 'builtin' 'unset' 'p10k_config_opts' diff --git a/run.py b/run.py index 69fc7b5..53dc9f8 100755 --- a/run.py +++ b/run.py @@ -17,7 +17,7 @@ ----------------- - **Inventory**: Manage the standalone inventory git repository - **Tool Installation**: Install Python tooling (uv, hatch) with retry logic -- **Codebase**: Clean build artifacts, run pre-commit hooks, generate Makefile +- **Codebase**: Clean build artifacts, run prek hooks, generate Makefile Key Features ----------- @@ -49,7 +49,7 @@ See Also ------- - README.md: User-facing documentation -- .claude/CLAUDE.md: Architecture and development guidance +- CLAUDE.md: Architecture and development guidance """ from __future__ import annotations @@ -391,12 +391,11 @@ def style( def _interpret_color(_color: StyleColor, offset: int = 0) -> str: if isinstance(_color, int): return f'{38 + offset};5;{_color:d}' - elif isinstance(_color, (tuple, list)): + if isinstance(_color, (tuple, list)): r, g, b = _color return f'{38 + offset};2;{r:d};{g:d};{b:d}' - else: - _color = cast('str', _color) - return str(_ansi_colors[_color] + offset) + _color = cast('str', _color) + return str(_ansi_colors[_color] + offset) if not isinstance(text, str): text = str(text) @@ -467,7 +466,7 @@ def printf( ) -> None: if debug and not RUN_DEBUG: return - elif debug: + if debug: fg = fg if fg is not None else 'bright_white' dim = dim if dim is not None else True file = sys.stderr @@ -644,7 +643,7 @@ def shell_command( **kwargs, ) except subprocess.CalledProcessError as e: - raise ShellCommandError(e.returncode, shlex.join(cmd), output=e.output, stderr=e.stderr) + raise ShellCommandError(e.returncode, shlex.join(cmd), output=e.output, stderr=e.stderr) from e except subprocess.TimeoutExpired as e: printf(f'❌ Command timed out after {timeout}s', fg='red', bold=True, indent=indent or 0) printf(f' └─ Command: {shlex.join(cmd)}', fg='red', indent=(indent or 0) + 1) @@ -700,8 +699,7 @@ def shell_command( if check and (returncode != 0): raise ShellCommandError(returncode, shlex.join(cmd), stderr=all_output) - else: - return subprocess.CompletedProcess(cmd, returncode, stdout=all_output) + return subprocess.CompletedProcess(cmd, returncode, stdout=all_output) def detect_ssh_error(e: subprocess.CalledProcessError) -> tuple[bool, str | None]: @@ -745,55 +743,55 @@ def handle_command_error(e: subprocess.CalledProcessError, context: str, *, sugg :param context: description of what operation failed :param suggestion: optional helpful suggestion for fixing the issue """ - printf(f'❌ {context}', fg='red', bold=True) - printf(f' └─ Exit code: {e.returncode}', fg='red', indent=1) - printf(f' └─ Command: {shlex.join(e.cmd)}', fg='red', indent=1) + printf(f'❌ {context}', fg='red', bold=True) + printf(f' └─ Exit code: {e.returncode}', fg='red') + printf(f' └─ Command: {shlex.join(e.cmd)}', fg='red') if e.stderr: stderr_text = e.stderr if isinstance(e.stderr, str) else e.stderr.decode() - printf(' └─ Error output:', fg='red', indent=1) + printf(' └─ Error output:', fg='red') for line in stderr_text.strip().split('\n')[:10]: - printf(f' {line}', fg='red', indent=1) + printf(f' {line}', fg='red') # Check for SSH-specific errors and provide tailored suggestions is_ssh_error, ssh_error_type = detect_ssh_error(e) if is_ssh_error and ssh_error_type: - printf(' └─ 🔑 SSH Authentication Issue Detected', fg='yellow', bold=True, indent=1) + printf(' └─ 🔑 SSH Authentication Issue Detected', fg='yellow', bold=True) if ssh_error_type == 'permission_denied': - printf(' └─ 💡 Your SSH key is not authorized for this repository', fg='yellow', indent=1) - printf(' Try these steps:', fg='yellow', indent=1) - printf(' 1. Check if SSH key exists: ls -la ~/.ssh/', fg='yellow', indent=1) - printf(' 2. Generate new key if needed: ssh-keygen -t ed25519 -C "your_email@example.com"', fg='yellow', indent=1) - printf(' 3. Add key to ssh-agent: ssh-add ~/.ssh/id_ed25519', fg='yellow', indent=1) - printf(' 4. Add public key to GitHub: https://github.com/settings/keys', fg='yellow', indent=1) - printf(' 5. Test connection: ssh -T git@github.com', fg='yellow', indent=1) + printf(' └─ 💡 Your SSH key is not authorized for this repository', fg='yellow') + printf(' Try these steps:', fg='yellow') + printf(' 1. Check if SSH key exists: ls -la ~/.ssh/', fg='yellow') + printf(' 2. Generate new key if needed: ssh-keygen -t ed25519 -C "email@example.com"', fg='yellow') + printf(' 3. Add key to ssh-agent: ssh-add ~/.ssh/id_ed25519', fg='yellow') + printf(' 4. Add public key to GitHub: https://github.com/settings/keys', fg='yellow') + printf(' 5. Test connection: ssh -T git@github.com', fg='yellow') elif ssh_error_type == 'host_key_verification': - printf(' └─ 💡 GitHub host key not recognized', fg='yellow', indent=1) - printf(' Run: ssh-keyscan github.com >> ~/.ssh/known_hosts', fg='yellow', indent=1) + printf(' └─ 💡 GitHub host key not recognized', fg='yellow') + printf(' Run: ssh-keyscan github.com >> ~/.ssh/known_hosts', fg='yellow') elif ssh_error_type in ('no_identities', 'key_load_failed'): - printf(' └─ 💡 SSH key could not be loaded', fg='yellow', indent=1) - printf(' Run: ssh-add ~/.ssh/id_ed25519 (or your key path)', fg='yellow', indent=1) + printf(' └─ 💡 SSH key could not be loaded', fg='yellow') + printf(' Run: ssh-add ~/.ssh/id_ed25519 (or your key path)', fg='yellow') elif ssh_error_type == 'connection_refused': - printf(' └─ 💡 SSH connection refused', fg='yellow', indent=1) - printf(' Check network connectivity and firewall settings', fg='yellow', indent=1) + printf(' └─ 💡 SSH connection refused', fg='yellow') + printf(' Check network connectivity and firewall settings', fg='yellow') elif ssh_error_type == 'connection_timeout': - printf(' └─ 💡 SSH connection timed out', fg='yellow', indent=1) - printf(' Check network connectivity and try again', fg='yellow', indent=1) + printf(' └─ 💡 SSH connection timed out', fg='yellow') + printf(' Check network connectivity and try again', fg='yellow') elif ssh_error_type == 'unknown_host': - printf(' └─ 💡 Could not resolve hostname', fg='yellow', indent=1) - printf(' Check your internet connection and DNS settings', fg='yellow', indent=1) + printf(' └─ 💡 Could not resolve hostname', fg='yellow') + printf(' Check your internet connection and DNS settings', fg='yellow') else: # generic SSH error - printf(' └─ 💡 Try testing SSH connection: ssh -T git@github.com', fg='yellow', indent=1) + printf(' └─ 💡 Try testing SSH connection: ssh -T git@github.com', fg='yellow') elif suggestion: - printf(f' └─ 💡 {suggestion}', fg='yellow', indent=1) + printf(f' └─ 💡 {suggestion}', fg='yellow') """ @@ -1372,7 +1370,7 @@ def cmd_install_hatch(args: argparse.Namespace) -> None: General repository maintenance commands: - clean: Remove build artifacts, caches, and temporary files -- pre: Run pre-commit hooks on all files +- pre: Run prek hooks on all files - makefile: Generate Makefile targets from @command-registered functions - Parses all registered commands and creates equivalent Make targets - Organizes targets by command group @@ -1410,11 +1408,37 @@ def cmd_clean(args: argparse.Namespace) -> None: @command(group='Codebase') def cmd_pre(args: argparse.Namespace) -> None: """ - Run pre-commit hooks on all project files + Run prek (pre-commit) hooks on all project files + """ + printf('🕝 Running prek hooks on all project files...', fg='bright_cyan') + shell_command( + ['prek', 'run', '--all-files'], + indent=3, + check=False, + ) + + +@command(group='Codebase') +def cmd_install_hooks(args: argparse.Namespace) -> None: + """ + Install prek git hook shims (overwrites any existing shims) + """ + printf('🪝 Installing prek git hook shims...', fg='bright_cyan') + shell_command( + ['prek', 'install', '-f'], + indent=3, + check=False, + ) + + +@command(group='Codebase') +def cmd_uninstall_hooks(args: argparse.Namespace) -> None: + """ + Uninstall prek git hook shims """ - printf('🕝 Running pre-commit hooks on all project files...', fg='bright_cyan') + printf('🪝 Uninstalling prek git hook shims...', fg='bright_cyan') shell_command( - ['pre-commit', 'run', '--all-files'], + ['prek', 'uninstall'], indent=3, check=False, ) diff --git a/scripts/env/README.md b/scripts/env/README.md new file mode 100644 index 0000000..6b48b38 --- /dev/null +++ b/scripts/env/README.md @@ -0,0 +1,266 @@ +[Project Root](../../README.md) > [Scripts](../README.md) > **Env Scripts** + +--- + +# Env Scripts + +Standalone Python utilities for inspecting, loading, and entering Python virtual environments. All three scripts are +zero-dependency where possible (standard library only) or declare their dependencies inline via PEP 723 script metadata, +making them safe to run without a pre-activated environment. + + + +- [Scripts](#scripts) +- [`loadenv.py`](#loadenvpy) + - [Synopsis](#synopsis) + - [Options](#options) + - [Behavior](#behavior) + - [Exit Codes](#exit-codes) + - [Usage Examples](#usage-examples) +- [`python_diagnostic.py`](#python_diagnosticpy) + - [Synopsis](#synopsis-1) + - [Options](#options-1) + - [Behavior](#behavior-1) + - [Exit Codes](#exit-codes-1) + - [Usage Examples](#usage-examples-1) +- [`venvshell.py`](#venvshellpy) + - [Synopsis](#synopsis-2) + - [Options](#options-2) + - [Behavior](#behavior-2) + - [Exit Codes](#exit-codes-2) + - [Usage Examples](#usage-examples-2) + + + +## Scripts + +| Script | What it does | Typical usage | +| ---------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------- | +| [`loadenv.py`](#loadenvpy) | Parse `.env` files; emit JSON, shell exports, or key-value pairs | `eval "$(python3 loadenv.py --export .env)"` | +| [`python_diagnostic.py`](#python_diagnosticpy) | Collect a full Python-environment diagnostic report | `python3 python_diagnostic.py` | +| [`venvshell.py`](#venvshellpy) | Spawn an interactive subshell with a virtualenv activated | `python3 venvshell.py` | + +--- + +## `loadenv.py` + +### Synopsis + +Standalone `.env` file parser. Parses a dotenv file and writes its contents to stdout in one of three formats. Parser +logic is vendored from `ezcli.dotenv` v1.6.13 and has no external dependencies. + +``` +python3 scripts/env/loadenv.py [OPTIONS] [FILE] +``` + +If `FILE` is omitted the script walks from the current working directory upward until it finds a `.env` file. + +### Options + +| Flag | Description | Default | +| ------------------ | ---------------------------------------------------------------------- | ----------------------------- | +| `FILE` | Path to `.env` file | auto-discover from CWD upward | +| `-j`, `--json` | Output as JSON object | yes (default mode) | +| `-e`, `--export` | Output as `export KEY="value"` statements | — | +| `-p`, `--pairs` | Output as `KEY=value` pairs | — | +| `--override` | `.env` values override existing env vars during `${VAR}` interpolation | off | +| `--no-interpolate` | Disable `${VAR}` variable expansion | interpolation on | +| `-q`, `--quiet` | Suppress warnings and errors to stderr | off | +| `-v`, `--version` | Print version and exit | — | +| `-h`, `--help` | Show help and exit | — | + +### Behavior + +1. Resolves the target file (explicit path or CWD-upward walk). +2. Parses each line into key-value `Binding` objects, handling single-quoted, double-quoted, and unquoted values, inline + comments, `export` prefixes, and blank lines. +3. Optionally resolves `${NAME}` and `${NAME:-default}` variable references against already-seen dotenv values and + `os.environ`. +4. Formats and writes the result to stdout. + +### Exit Codes + +| Code | Meaning | +| ---- | -------------------------------------------------- | +| `0` | Success (no parse errors) | +| `1` | File not found or could not be opened | +| `2` | Success, but one or more lines could not be parsed | + +### Usage Examples + +```bash +# Source variables into the current shell +eval "$(python3 scripts/env/loadenv.py --export .env)" + +# Inspect parsed values as JSON +python3 scripts/env/loadenv.py --json .env + +# Emit raw KEY=value pairs (useful for piping into other tools) +python3 scripts/env/loadenv.py --pairs .env + +# Disable variable interpolation +python3 scripts/env/loadenv.py --no-interpolate .env + +# Suppress parse warnings while still failing on missing file +python3 scripts/env/loadenv.py -q .env || echo "file not found" +``` + +--- + +## `python_diagnostic.py` + +### Synopsis + +Collects and formats a comprehensive Python-environment diagnostic report covering system info, all Python executables +on `PATH`, virtual-environment state, package managers (`pip`, `uv`, `hatch`), key environment variables, `sys.path`, +common installation locations, and installed packages. + +``` +python3 scripts/env/python_diagnostic.py [OPTIONS] +``` + +### Options + +| Flag | Description | Default | +| ---------------------------------- | ----------------------------------------------------- | ----------------------------------- | +| `--max-installed-packages NUM` | Truncate the installed-packages list to `NUM` entries | no limit | +| `-s`, `--save` | Save output to a timestamped file instead of printing | off | +| `-j`, `--json` | Output raw JSON instead of formatted text | off | +| `-o FILEPATH`, `--output FILEPATH` | Explicit output file path (implies `--save`) | `python_diagnostic_.txt` | +| `-h`, `--help` | Show help and exit | — | + +### Behavior + +The script runs a collection phase followed by a formatting/output phase. + +**Collection phase** — `collect_diagnostics()` gathers: + +- `system` — `platform.platform()`, `sw_vers`, `uname -a`, Python version and implementation +- `python_executables` — resolves `python`, `python3`, `python3.9`–`python3.13` from `PATH` via `shutil.which` +- `virtual_env` — `VIRTUAL_ENV`, `CONDA_DEFAULT_ENV`, `sys.prefix` / `sys.base_prefix` / `sys.real_prefix` +- `package_managers` — pip path/version, `uv` path/version/managed-Python list (`uv python list --json`), hatch + path/version/environments (`hatch env show --json`) +- `environment_variables` — a curated allowlist of non-sensitive vars (e.g. `PYTHONPATH`, `UV_INDEX_URL`, `PATH`) +- `python_paths` — `sys.executable`, `sys.path`, site-packages entries +- `common_locations` — existence/symlink check for system Python, Homebrew, pyenv, `~/.local/bin`, etc. +- `installed_packages` — `uv pip list --format=json` (falls back to `pip list`) + +**Output phase** — either `format_diagnostic_output()` (human-readable text) or `json.dumps()` (structured JSON), +written to stdout or a file. + +### Exit Codes + +| Code | Meaning | +| ---- | ------------------------------------------------------------------- | +| `0` | Always (diagnostic failures are reported inline, not as exit codes) | + +### Usage Examples + +```bash +# Print to stdout (default) +python3 scripts/env/python_diagnostic.py + +# Limit installed package listing to avoid wall-of-text +python3 scripts/env/python_diagnostic.py --max-installed-packages 20 + +# Save full JSON report for sharing or diffing +python3 scripts/env/python_diagnostic.py --json --output /tmp/pydiag.json + +# Auto-timestamped text file +python3 scripts/env/python_diagnostic.py --save +# => Saves to python_diagnostic_20240101_120000.txt +``` + +--- + +## `venvshell.py` + +### Synopsis + +Spawns an interactive subshell with a Python virtual environment activated. Supports `bash`, `zsh`, and `fish`. Handles +shell history preservation for zsh (works around `ZDOTDIR` override side effects), temp-dir cleanup via `atexit`, and +fish `activate.fish` sourcing. Also supports hatch-managed environments via `hatch env find`. + +``` +python3 scripts/env/venvshell.py [OPTIONS] [PATH] +``` + +### Options + +#### Environment selection + +| Flag | Description | Env var override | Default | +| ------------- | -------------------------------------------------------- | --------------------- | ------- | +| `PATH` | Positional path to venv directory | `VENVSHELL_PATH` | — | +| `--path PATH` | Path to venv directory (named alternative to positional) | `VENVSHELL_PATH` | — | +| `--hatch ENV` | Hatch environment name | `VENVSHELL_HATCH_ENV` | — | + +#### Environment discovery (when no explicit path given) + +| Flag | Description | Env var override | Default | +| ------------------------------------------ | -------------------------------------------- | -------------------------- | ------- | +| `--discover` / `--no-discover` | Enable/disable auto-discovery | `VENVSHELL_DISCOVER` | `True` | +| `--discover-hatch` / `--no-discover-hatch` | Include hatch environments in auto-discovery | `VENVSHELL_DISCOVER_HATCH` | `True` | + +#### Output + +| Flag | Description | +| ----------------- | -------------------------------------------------------------------------- | +| `-v`, `--verbose` | Print debug messages to stderr (discovery steps, shell args, env vars set) | +| `-h`, `--help` | Show help and exit | + +### Behavior + +**Resolution order** (first match wins): + +1. Explicit `--path PATH` or positional `PATH` argument +2. Explicit `--hatch ENV` (runs `hatch env find ENV`) +3. Auto-discovered `.venv/` or `venv/` directory (walks up from CWD) +4. Auto-discovered default hatch environment (`hatch env find`) + +**Shell activation** — launches via `subprocess.run` (not `os.execvpe`) so `atexit` cleanup of temp directories runs on +exit: + +- **bash** — writes a temp `.bashrc` that sources the user's real dotfiles then `source activate` +- **zsh** — creates a temp `ZDOTDIR`, symlinks all non-`.zshrc` dotfiles from the real `ZDOTDIR`, generates a custom + `.zshrc` that sources the real `.zshrc`, activates the venv, then hard-codes `HISTFILE` to the resolved real history + file path (prevents history loss when `ZDOTDIR` is overridden) +- **fish** — passes `-C "source activate.fish"` to the shell process +- **other** — activates via env vars only (`VIRTUAL_ENV`, `PATH` prepend) with a warning + +**Guard rails** — if `VIRTUAL_ENV` is already set, the script exits with a warning rather than nesting environments. + +### Exit Codes + +| Code | Meaning | +| ---- | -------------------------------------------------------------------------- | +| `0` | Venv activated and subshell exited normally, or environment already active | +| `1` | Existing (different) environment already active | +| `64` | Invalid arguments (`EX_USAGE`) | +| `65` | Venv directory is broken/invalid (`EX_DATAERR`) | +| `69` | No virtual environment found (`EX_UNAVAILABLE`) | +| `90` | Shell activation failed | + +### Usage Examples + +```bash +# Auto-discover venv in current project +python3 scripts/env/venvshell.py + +# Explicitly specify a venv path +python3 scripts/env/venvshell.py .venv +python3 scripts/env/venvshell.py --path /path/to/project/.venv + +# Use a hatch-managed environment +python3 scripts/env/venvshell.py --hatch default + +# Disable hatch discovery fallback (only look for .venv/ or venv/ dirs) +python3 scripts/env/venvshell.py --no-discover-hatch + +# Debug discovery steps +python3 scripts/env/venvshell.py --verbose + +# Hard-code default venv path via environment variable (e.g. in shell profile) +export VENVSHELL_PATH=~/projects/myapp/.venv +python3 scripts/env/venvshell.py +``` diff --git a/scripts/env/loadenv.py b/scripts/env/loadenv.py index 3e630b1..bc1287b 100755 --- a/scripts/env/loadenv.py +++ b/scripts/env/loadenv.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Standalone dotenv file parser and environment variable loader +Standalone dotenv file parser and environment variable loader. Parses .env files and outputs values in JSON, shell export, or key-value pair format @@ -43,16 +43,9 @@ PathLike = Union[str, Path] -class Error(Exception): - """ - Parsing error raised when a regex match or read operation fails within the :class:`Reader` - """ - ... - - -# ======= -# Regexes -# ======= +# ================================================================================================== +# Vendored from ezcli.dotenv v1.6.13 (parser.py) +# ================================================================================================== _newline = re.compile(r'(\r\n|\n|\r)', re.UNICODE) _multiline_whitespace = re.compile(r'\s*', re.UNICODE | re.MULTILINE) @@ -70,27 +63,17 @@ class Error(Exception): _double_quote_escapes = re.compile(r"\\[\\'\"abfnrtv]", re.UNICODE) _single_quote_escapes = re.compile(r"\\[\\']", re.UNICODE) -_posix_variable = re.compile( - r""" - \$\{ - (?P[^\}:]*) - (?::- - (?P[^\}]*) - )? - \} - """, - re.VERBOSE, -) - -# ======= -# Parsing -# ======= +class Error(Exception): + """ + Parsing error raised when a regex match or read operation fails within the :class:`Reader`. + """ + pass class Original(NamedTuple): """ - The original text of a parsed line and its starting line number + The original text of a parsed line and its starting line number. Preserves the raw string content so that lines can be rewritten verbatim when modifying a `.env` file """ @@ -100,7 +83,7 @@ class Original(NamedTuple): class Binding(NamedTuple): """ - A single parsed key-value binding from a `.env` file + A single parsed key-value binding from a `.env` file. Represents one logical line of a dotenv file after parsing. Lines that could not be parsed have `error=True` with `key` and `value` set to `None` @@ -113,7 +96,7 @@ class Binding(NamedTuple): class Position: """ - Tracks the current character offset and line number within a dotenv source string + Tracks the current character offset and line number within a dotenv source string. Used by :class:`Reader` to maintain cursor state as the parser advances through the input @@ -127,7 +110,7 @@ def __init__(self, chars: int, line: int) -> None: @classmethod def start(cls) -> Position: """ - Create a position representing the beginning of a source string + Create a position representing the beginning of a source string. :return: a new :class:`Position` at character 0, line 1 """ @@ -135,7 +118,7 @@ def start(cls) -> Position: def set(self, other: Position) -> None: """ - Copy the character offset and line number from another position + Copy the character offset and line number from another position. :param other: the position to copy from """ @@ -144,7 +127,7 @@ def set(self, other: Position) -> None: def advance(self, string: str) -> None: """ - Advance the position by the length of the given string, counting newlines + Advance the position by the length of the given string, counting newlines. :param string: the text that was consumed from the source """ @@ -154,7 +137,7 @@ def advance(self, string: str) -> None: class Reader: """ - Buffered reader that tokenizes a dotenv source string using regex-based pattern matching + Buffered reader that tokenizes a dotenv source string using regex-based pattern matching. Reads the entire stream into memory, then provides methods to consume characters and match regex patterns while tracking the current :class:`Position`. Supports marking regions of text so that the original raw content can be @@ -169,7 +152,7 @@ def __init__(self, stream: IO[str]) -> None: def has_next(self) -> bool: """ - Check if there are remaining characters to read + Check if there are remaining characters to read. :return: ``True`` if the cursor has not reached the end of the string """ @@ -177,13 +160,13 @@ def has_next(self) -> bool: def set_mark(self) -> None: """ - Save the current position as a mark for later retrieval via :meth:`get_marked` + Save the current position as a mark for later retrieval via :meth:`get_marked`. """ self.mark.set(self.position) def get_marked(self) -> Original: """ - Return the text between the last mark and the current position as an :class:`Original` + Return the text between the last mark and the current position as an :class:`Original`. :return: the raw substring and starting line number since :meth:`set_mark` was last called """ @@ -194,7 +177,7 @@ def get_marked(self) -> Original: def peek(self, count: int) -> str: """ - Return upcoming characters without advancing the position + Return upcoming characters without advancing the position. :param count: number of characters to peek at :return: up to `count` characters from the current position @@ -203,7 +186,7 @@ def peek(self, count: int) -> str: def read(self, count: int) -> str: """ - Consume and return the next `count` characters, advancing the position + Consume and return the next `count` characters, advancing the position. :param count: number of characters to read :raises Error: if fewer than `count` characters remain @@ -217,7 +200,7 @@ def read(self, count: int) -> str: def read_regex(self, regex: re.Pattern[str]) -> Sequence[str]: """ - Match a regex at the current position, consume the matched text, and return capture groups + Match a regex at the current position, consume the matched text, and return capture groups. :param regex: compiled regex pattern to match at the current cursor position :raises Error: if the pattern does not match at the current position @@ -232,7 +215,7 @@ def read_regex(self, regex: re.Pattern[str]) -> Sequence[str]: def parse_key(reader: Reader) -> str | None: """ - Parse a dotenv key from the current reader position + Parse a dotenv key from the current reader position. Handles both single-quoted keys (e.g. ``"MY KEY"``) and unquoted keys. Lines starting with "#" are treated as comments and return `None` @@ -243,16 +226,29 @@ def parse_key(reader: Reader) -> str | None: char = reader.peek(1) if char == '#': return None - elif char == "'": + if char == "'": key, *_ = reader.read_regex(_single_quoted_key) else: key, *_ = reader.read_regex(_unquoted_key) return key -def parse_value(reader: Reader) -> str: +def _decode_escapes(regex: re.Pattern[str], string: str) -> str: """ - Parse a dotenv value from the current reader position + Replace backslash escape sequences matched by the given regex with their decoded Unicode equivalents. + + :param regex: compiled pattern matching escape sequences to decode + :param string: the string containing escape sequences + :return: string with matched escape sequences decoded + """ + def decode_match(match: re.Match[str]) -> str: + return codecs.decode(match.group(0), 'unicode-escape') + return regex.sub(decode_match, string) + + +def parse_value(reader: Reader) -> str: + r""" + Parse a dotenv value from the current reader position. Handles single-quoted values (with ``\\'`` escapes), double-quoted values (with standard backslash escapes), unquoted values (trimming inline comments and trailing whitespace), and empty values @@ -260,28 +256,22 @@ def parse_value(reader: Reader) -> str: :param reader: the :class:`Reader` positioned at the start of a value (after the "=") :return: the parsed and unescaped value string """ - def decode_escapes(regex: re.Pattern[str], string: str) -> str: - def decode_match(match: re.Match[str]) -> str: - return codecs.decode(match.group(0), 'unicode-escape') - return regex.sub(decode_match, string) - char = reader.peek(1) if char == "'": value, *_ = reader.read_regex(_single_quoted_value) - return decode_escapes(_single_quote_escapes, value) - elif char == '"': + return _decode_escapes(_single_quote_escapes, value) + if char == '"': value, *_ = reader.read_regex(_double_quoted_value) - return decode_escapes(_double_quote_escapes, value) - elif char in ('', '\n', '\r'): + return _decode_escapes(_double_quote_escapes, value) + if char in ('', '\n', '\r'): return '' - else: - part, *_ = reader.read_regex(_unquoted_value) - return re.sub(r'\s+#.*', '', part).rstrip() + part, *_ = reader.read_regex(_unquoted_value) + return re.sub(r'\s+#.*', '', part).rstrip() def parse_binding(reader: Reader) -> Binding: """ - Parse a single key-value binding from the current reader position + Parse a single key-value binding from the current reader position. Consumes one logical line of dotenv content including leading whitespace, optional ``export`` prefix, key, equals sign, value, inline comment, and line ending. If parsing fails, the remainder of the line is consumed @@ -331,7 +321,7 @@ def parse_binding(reader: Reader) -> Binding: def parse_stream(stream: IO[str]) -> Iterator[Binding]: """ - Parse an entire dotenv stream into a sequence of :class:`Binding` objects + Parse an entire dotenv stream into a sequence of :class:`Binding` objects. Reads the stream and yields one :class:`Binding` per logical line until all content has been consumed @@ -343,19 +333,32 @@ def parse_stream(stream: IO[str]) -> Iterator[Binding]: yield parse_binding(reader) -# =============== -# Variables (AST) -# =============== +# ================================================================================================== +# Vendored from ezcli.dotenv v1.6.13 (variables.py) +# ================================================================================================== + + +_posix_variable: re.Pattern[str] = re.compile( + r""" + \$\{ + (?P[^\}:]*) + (?::- + (?P[^\}]*) + )? + \} + """, + re.VERBOSE, +) class Atom(metaclass=abc.ABCMeta): """ - Abstract base class for parsed components of a dotenv value string + Abstract base class for parsed components of a dotenv value string. Each atom represents either a literal text segment or a variable reference that can be resolved against an environment mapping """ - def __ne__(self, other: Any) -> bool: + def __ne__(self, other: object) -> bool: result = self.__eq__(other) if result is NotImplemented: return NotImplemented @@ -364,7 +367,7 @@ def __ne__(self, other: Any) -> bool: @abc.abstractmethod def resolve(self, env: Mapping[str, str | None]) -> str: """ - Resolve this atom to a concrete string value + Resolve this atom to a concrete string value. :param env: environment mapping of variable names to their values :return: the resolved string value @@ -374,7 +377,7 @@ def resolve(self, env: Mapping[str, str | None]) -> str: class Literal(Atom): """ - A literal text segment within a dotenv value + A literal text segment within a dotenv value. Represents a portion of a value string that contains no variable references and resolves to its stored text verbatim @@ -387,18 +390,17 @@ def __init__(self, value: str) -> None: def __repr__(self) -> str: return f'Literal(value={self.value})' + def __eq__(self, other: object) -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + return self.value == other.value + def __hash__(self) -> int: return hash((self.__class__, self.value)) - def __eq__(self, other: Any) -> bool: - if isinstance(other, self.__class__): - return self.value == other.value - else: - return NotImplemented - def resolve(self, env: Mapping[str, str | None]) -> str: """ - Return the literal value unchanged + Return the literal value unchanged. :param env: environment mapping (unused for literal) :return: the stored literal text @@ -408,7 +410,7 @@ def resolve(self, env: Mapping[str, str | None]) -> str: class Variable(Atom): """ - A POSIX-style variable reference within a dotenv value + A POSIX-style variable reference within a dotenv value. Represents a ``${NAME}`` or ``${NAME:-default}`` reference that resolves by looking up the variable name in the provided environment mapping, falling back to the default value when the variable is not found @@ -423,21 +425,20 @@ def __init__(self, name: str, default: str | None) -> None: def __repr__(self) -> str: return f'Variable(name={self.name}, default={self.default})' + def __eq__(self, other: object) -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + return (self.name, self.default) == (other.name, other.default) + def __hash__(self) -> int: return hash((self.__class__, self.name, self.default)) - def __eq__(self, other: Any) -> bool: - if isinstance(other, self.__class__): - return (self.name, self.default) == (other.name, other.default) - else: - return NotImplemented - def resolve(self, env: Mapping[str, str | None]) -> str: """ - Resolve the variable reference against the given environment + Resolve the variable reference against the given environment. - Looks up :attr:`name` in `env`. If not found, or the value is `None`, falls back to :attr:`default` - (or an empty string if no default was specified) + Looks up :attr:`name` in `env`. If not found, or the value is `None`, falls back to :attr:`default` (or an + empty string if no default was specified) :param env: environment mapping of variable names to their values :return: the resolved value from the environment, the default, or an empty string @@ -449,7 +450,7 @@ def resolve(self, env: Mapping[str, str | None]) -> str: def parse_variables(value: str) -> Iterator[Atom]: """ - Parse a dotenv value string into a sequence of :class:`Atom` nodes + Parse a dotenv value string into a sequence of :class:`Atom` nodes. Scans for POSIX-style ``${NAME}`` and ``${NAME:-default}`` variable references, yielding :class:`Variable` nodes for each match and :class:`Literal` nodes for the text between them @@ -473,19 +474,19 @@ def parse_variables(value: str) -> Iterator[Atom]: yield Literal(value=value[cursor:length]) -# ========== -# Resolution -# ========== +# ================================================================================================== +# Vendored from ezcli.dotenv v1.6.13 (main.py) +# ================================================================================================== def resolve_variables(values: Iterable[tuple[str, str | None]], override: bool) -> Mapping[str, str | None]: """ - Resolve ``${VAR}``-style variable references across a sequence of key-value pairs + Resolve ``${VAR}``-style variable references across a sequence of key-value pairs. Processes values in order, building up a mapping of resolved names. Each value's variable references are resolved against the combination of previously resolved values and :attr:`os.environ`. - When `override=True`, :attr:`os.environ` takes lower precedence than already-resolved dotenv values. + When `override=True`, :attr:`os.environ` takes lower precedence than already-resolved dotenv values. When `override=False`, :attr:`os.environ` values take priority :param values: iterable of ``(key, value)`` tuples with unresolved variable references @@ -509,13 +510,12 @@ def resolve_variables(values: Iterable[tuple[str, str | None]], override: bool) result = ''.join(atom.resolve(env) for atom in atoms) new_values[name] = result - return new_values def _walk_to_root(path: PathLike) -> Iterator[Path]: """ - Yield directories starting from the given path up to the filesystem root + Yield directories starting from the given path up to the filesystem root. If `path` points to a file, iteration begins from its parent directory. Each iteration yields the next parent directory until the root is reached @@ -541,7 +541,7 @@ def _walk_to_root(path: PathLike) -> Iterator[Path]: def find_dotenv(filename: str = '.env', *, raise_error_if_not_found: bool = False) -> str: """ - Search from CWD upward for the given dotenv file + Search from CWD upward for the given dotenv file. :param filename: the dotenv filename to search for :param raise_error_if_not_found: whether to raise :class:`OSError` if the file is not found @@ -566,7 +566,7 @@ def dotenv_values( quiet: bool = False, ) -> tuple[dict[str, str | None], bool]: """ - Parse a .env file and return (values_dict, has_errors) + Parse a .env file and return (values_dict, has_errors). :param dotenv_path: absolute or relative path to the `.env` file :param interpolate: whether to resolve ``${VAR}``-style variable references in values @@ -592,13 +592,12 @@ def dotenv_values( if interpolate: return dict(resolve_variables(raw_values, override=override)), has_errors - else: - return dict(raw_values), has_errors + return dict(raw_values), has_errors -# ================= -# Output formatting -# ================= +# ================================================================================================== +# Output formatters +# ================================================================================================== OutputFormat = LiteralStr['json', 'export', 'pairs'] @@ -606,18 +605,17 @@ def dotenv_values( def _shell_escape(value: str) -> str: """ - Escape a value for safe use inside double-quoted shell strings + Escape a value for safe use inside double-quoted shell strings. """ value = value.replace('\\', '\\\\') value = value.replace('"', '\\"') value = value.replace('$', '\\$') - value = value.replace('`', '\\`') - return value + return value.replace('`', '\\`') def format_json(values: dict[str, str | None], **kwargs: Any) -> str: """ - Format parsed values as a JSON object + Format parsed values as a JSON object. """ return json.dumps( values, @@ -628,10 +626,11 @@ def format_json(values: dict[str, str | None], **kwargs: Any) -> str: def format_export(values: dict[str, str | None]) -> str: """ - Format parsed values as shell export statements + Format parsed values as shell export statements. """ lines: list[str] = [ - f'export {key}="{_shell_escape(val)}"' for key, val in values.items() + f'export {key}="{_shell_escape(val)}"' + for key, val in values.items() if val is not None ] return '\n'.join(lines) + '\n' if lines else '' @@ -639,10 +638,11 @@ def format_export(values: dict[str, str | None]) -> str: def format_pairs(values: dict[str, str | None]) -> str: """ - Format parsed values as KEY=value pairs + Format parsed values as KEY=value pairs. """ lines: list[str] = [ - key if val is None else f'{key}={val}' for key, val in values.items() + key if val is None else f'{key}={val}' + for key, val in values.items() ] return '\n'.join(lines) + '\n' if lines else '' @@ -654,25 +654,25 @@ def format_pairs(values: dict[str, str | None]) -> str: } -# ============================================ -# Argument parsing and core script entry point -# ============================================ +# ================================================================================================== +# Script CLI entry point +# ================================================================================================== class Args(argparse.Namespace): """ - Annotated :class:`argparse.Namespace` for script command-line arguments, returned by :func:`parse_args` + Annotated :class:`argparse.Namespace` returned by :func:`parse_args`. """ - file: Path | None # positional script arg - mode: OutputFormat # --json/--export/--pairs - override: bool # --override - no_interpolate: bool # --no-interpolate - quiet: bool # --quiet + file: Path | None # positional script arg + mode: OutputFormat # --json/--export/--pairs + override: bool # --override + interpolate: bool # --no-interpolate + quiet: bool # --quiet def parse_args(argv: Sequence[str] | None = None) -> Args: """ - Parse command-line arguments + Parse command-line arguments. :param argv: argument list to parse, defaults to sys.argv[1:] :return: parsed namespace (:class:`Args`) @@ -687,10 +687,13 @@ def parse_args(argv: Sequence[str] | None = None) -> Args: ' %(prog)s --pairs .env Output as KEY=value pairs\n' ' eval "$(%(prog)s --export .env)" Source into current shell\n' ), - add_help=False + add_help=False, ) def add_positional_args() -> None: + """ + Register the positional arguments group on the parser. + """ parser.add_argument( 'file', nargs='?', @@ -699,10 +702,14 @@ def add_positional_args() -> None: metavar='FILE', help='path to .env file (default: auto-discover from current working directory)', ) + add_positional_args() - def add_mode_opts() -> None: - mode_opts = parser.add_argument_group('output format options') - mode_opts.add_argument( + def add_output_format_opts() -> None: + """ + Register the output format options group on the parser. + """ + output_format_opts = parser.add_argument_group('output format options') + output_format_opts.add_argument( '-j', '--json', dest='mode', @@ -711,7 +718,7 @@ def add_mode_opts() -> None: default='json', help='output as JSON object (default)', ) - mode_opts.add_argument( + output_format_opts.add_argument( '-e', '--export', dest='mode', @@ -719,7 +726,7 @@ def add_mode_opts() -> None: const='export', help='output as export KEY="value" statements', ) - mode_opts.add_argument( + output_format_opts.add_argument( '-p', '--pairs', dest='mode', @@ -727,8 +734,12 @@ def add_mode_opts() -> None: const='pairs', help='output as KEY=value pairs', ) + add_output_format_opts() def add_resolution_opts() -> None: + """ + Register the variable resolution options group on the parser. + """ resolution_opts = parser.add_argument_group('variable resolution options') resolution_opts.add_argument( '--override', @@ -743,8 +754,12 @@ def add_resolution_opts() -> None: default=True, help='disable ${VAR} variable expansion', ) + add_resolution_opts() def add_other_opts() -> None: + """ + Register the miscellaneous options group on the parser. + """ other_opts = parser.add_argument_group('other options') other_opts.add_argument( '-q', @@ -767,10 +782,6 @@ def add_other_opts() -> None: default=argparse.SUPPRESS, help='show this help message and exit', ) - - add_positional_args() - add_mode_opts() - add_resolution_opts() add_other_opts() return parser.parse_args(argv, namespace=Args()) @@ -778,13 +789,15 @@ def add_other_opts() -> None: def main(argv: Sequence[str] | None = None) -> int: """ - Standalone dotenv file parser and environment variable loader + Standalone dotenv file parser and environment variable loader. Parses .env files and outputs values in JSON, shell export, or key-value pair format :param argv: argument list to parse, defaults to sys.argv[1:] :return: int script exit code """ + + # Parse command-line arguments args = parse_args(argv) # Resolve file path diff --git a/scripts/env/python_diagnostic.py b/scripts/env/python_diagnostic.py index 2d2667c..e1030b1 100755 --- a/scripts/env/python_diagnostic.py +++ b/scripts/env/python_diagnostic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Python Environment Diagnostic Tool +Python Environment Diagnostic Tool. This script collects comprehensive information about a user's Python environment to help diagnose installation and configuration issues. @@ -58,10 +58,16 @@ class ErrorDict(TypedDict): + """ + Error information from a failed command. + """ error: str class RunCommandResult(TypedDict, total=False): + """ + Result of running a subprocess command. + """ stdout: str stderr: str returncode: int @@ -71,7 +77,7 @@ class RunCommandResult(TypedDict, total=False): def run_command(cmd: list[str], **kwargs: Any) -> RunCommandResult: """ - Run a command and return output, error, and return code + Run a command and return output, error, and return code. :param cmd: command to run as list of arguments :return: dict with stdout, stderr, returncode, and error flag @@ -106,6 +112,9 @@ def run_command(cmd: list[str], **kwargs: Any) -> RunCommandResult: class PythonExecutable(TypedDict): + """ + Information about a discovered Python executable. + """ path: str real_path: str version: str @@ -114,7 +123,7 @@ class PythonExecutable(TypedDict): def get_python_executables() -> dict[str, PythonExecutable]: """ - Find all Python executables in PATH + Find all Python executables in PATH. :return: mapping from executable name -> path/version info (:class:`PythonExecutable`) """ @@ -138,24 +147,35 @@ def get_python_executables() -> dict[str, PythonExecutable]: class VersionParts(TypedDict): + """ + Parsed semantic version components. + """ major: int minor: int patch: int -""" `pip` info types """ +# `pip` info types +# ---------------- class PipInfo(TypedDict): + """ + Pip installation paths and version. + """ pip_path: str | None pip3_path: str | None version: str -""" `uv` info types """ +# `uv` info types +# --------------- class UvInstalledPython(TypedDict): + """ + Details of a single Python installation managed by uv. + """ key: str version: str version_parts: VersionParts @@ -170,6 +190,9 @@ class UvInstalledPython(TypedDict): class UvInstalledPythons(TypedDict): + """ + Collection of Python installations managed by uv. + """ installed: list[UvInstalledPython] @@ -177,15 +200,22 @@ class UvInstalledPythons(TypedDict): class UvInfo(TypedDict): + """ + Uv tool path, version, and managed Python installations. + """ path: str version: str pythons: UvPythons -""" `hatch` info types """ +# `hatch` info types +# ------------------ class HatchEnvironment(TypedDict): + """ + Configuration for a single Hatch virtual environment. + """ type: Literal['virtual'] python: str installer: str @@ -195,6 +225,9 @@ class HatchEnvironment(TypedDict): class HatchInfo(TypedDict): + """ + Hatch tool path, version, and environment configuration. + """ path: str version: str environments: HatchEnvironments @@ -202,7 +235,7 @@ class HatchInfo(TypedDict): class PackageManagerInfo(TypedDict, total=False): """ - Dict schema with info about a package manager (values in dict returned by :func:`get_package_manager_info`) + Dict schema with info about a package manager (values in dict returned by :func:`get_package_manager_info`). """ pip: PipInfo uv: UvInfo @@ -211,7 +244,7 @@ class PackageManagerInfo(TypedDict, total=False): def get_package_manager_info() -> PackageManagerInfo: """ - Get information about package managers (pip, uv, hatch) + Get information about package managers (pip, uv, hatch). :return: mapping from package manager name -> path/version info (:class:`PackageManagerInfo`) """ @@ -290,6 +323,9 @@ def get_package_manager_info() -> PackageManagerInfo: class VirtualenvInfo(TypedDict): + """ + Current virtual environment state and prefix paths. + """ in_virtualenv: bool virtual_env: str | None conda_env: str | None @@ -300,7 +336,7 @@ class VirtualenvInfo(TypedDict): def get_virtual_env_info() -> VirtualenvInfo: """ - Get information about current virtual environment + Get information about current virtual environment. :return: dict with virtual environment details (:class:`VirtualenvInfo`) """ @@ -323,7 +359,7 @@ def get_virtual_env_info() -> VirtualenvInfo: def get_environment_variables() -> dict[str, str | None]: """ - Get relevant environment variables (non-sensitive) + Get relevant environment variables (non-sensitive). :return: dict of environment variables """ @@ -355,6 +391,9 @@ def get_environment_variables() -> dict[str, str | None]: class PythonPaths(TypedDict): + """ + Python interpreter paths and site-packages locations. + """ sys_executable: str sys_path: list[str] site_packages: list[str] @@ -364,7 +403,7 @@ class PythonPaths(TypedDict): def get_python_paths() -> PythonPaths: """ - Get Python-specific paths and configuration + Get Python-specific paths and configuration. :return: dict with Python paths (:class;`PythonPaths`) """ @@ -383,6 +422,9 @@ def get_python_paths() -> PythonPaths: class SystemInfo(TypedDict): + """ + Host system platform, architecture, and OS version details. + """ platform: str machine: str processor: str @@ -394,7 +436,7 @@ class SystemInfo(TypedDict): def get_system_info() -> SystemInfo: """ - Get macOS system information + Get macOS system information. :return: dict with system details (:class:`SystemInfo`) """ @@ -417,6 +459,9 @@ def get_system_info() -> SystemInfo: class PythonInstallation(TypedDict): + """ + Existence and symlink info for a Python installation at a known path. + """ exists: bool description: str is_symlink: bool | None @@ -425,7 +470,7 @@ class PythonInstallation(TypedDict): def check_common_locations() -> dict[str, PythonInstallation]: """ - Check for Python installations in common locations + Check for Python installations in common locations. :return: mapping from paths -> python installation info/existence info (:class:`PythonInstallation`) """ @@ -456,6 +501,9 @@ def check_common_locations() -> dict[str, PythonInstallation]: class InstalledPythonVersions(TypedDict): + """ + Installed Python package versions and editable project locations. + """ versions: dict[str, str] # package name -> version editable_locations: dict[str, str] # package name -> editable project location (path) @@ -468,7 +516,7 @@ class InstalledPythonVersions(TypedDict): def get_installed_packages() -> InstalledPythonPackages: """ - Get list of installed packages in current environment + Get list of installed packages in current environment. :return: dict containing "versions" (mapping from package name -> version) and "editable_locations" (mapping from package name -> editable project location), or dict with single "error" entry if `pip` command fails @@ -480,20 +528,19 @@ def get_installed_packages() -> InstalledPythonPackages: if pip_list.get('error'): return ErrorDict(error=pip_list.get('message', 'Unknown error')) - elif pip_list.get('returncode') != 0: + if pip_list.get('returncode') != 0: return ErrorDict(error=f"`pip list` failed: {pip_list.get('stderr', 'Unknown error')}") + try: + pip_list_packages = json.loads(pip_list['stdout']) + except (json.JSONDecodeError, KeyError) as e: + return ErrorDict(error=f'Failed to parse `pip list` output: {e}') else: - try: - pip_list_packages = json.loads(pip_list['stdout']) - except (json.JSONDecodeError, KeyError) as e: - return ErrorDict(error=f'Failed to parse `pip list` output: {e}') - else: - packages: InstalledPythonVersions = InstalledPythonVersions(versions={}, editable_locations={}) - for pkg in pip_list_packages: - packages['versions'][pkg['name']] = pkg['version'] - if editable_location := pkg.get('editable_project_location'): - packages['editable_locations'][pkg['name']] = editable_location - return packages + packages: InstalledPythonVersions = InstalledPythonVersions(versions={}, editable_locations={}) + for pkg in pip_list_packages: + packages['versions'][pkg['name']] = pkg['version'] + if editable_location := pkg.get('editable_project_location'): + packages['editable_locations'][pkg['name']] = editable_location + return packages # ============================================ @@ -502,6 +549,9 @@ def get_installed_packages() -> InstalledPythonPackages: class Diagnostics(TypedDict): + """ + Complete Python environment diagnostic report. + """ timestamp: str system: SystemInfo python_executables: dict[str, PythonExecutable] @@ -515,7 +565,7 @@ class Diagnostics(TypedDict): def collect_diagnostics() -> Diagnostics: """ - Collect all diagnostic information + Collect all diagnostic information. :return: complete diagnostic data dict (:class:`Diagnostics`) """ @@ -534,11 +584,11 @@ def collect_diagnostics() -> Diagnostics: def format_diagnostic_output(data: Diagnostics, *, max_installed_packages: int | None = None) -> str: """ - Format diagnostic data for human-readable output + Format diagnostic data for human-readable output. :param data: diagnostic data dict - :param max_installed_packages: maximum number of installed python packages to list in "INSTALLED PACKAGES" section before - truncating list. If None, all installed packages will be outputted + :param max_installed_packages: maximum number of installed python packages to list in "INSTALLED PACKAGES" section + before truncating list. If None, all installed packages will be outputted :return: formatted string output """ @@ -661,9 +711,10 @@ def format_diagnostic_output(data: Diagnostics, *, max_installed_packages: int | lines.append(f'{var_ljust} {value}') if path_envvar := data['environment_variables'].get('PATH'): - lines.append('\nPATH:') - for path in path_envvar.split(':'): - lines.append(f' {path}') + lines.extend([ + '\nPATH:', + *[f' {path}' for path in path_envvar.split(os.pathsep)], + ]) """ Python paths from :func:`get_python_paths` """ @@ -673,11 +724,9 @@ def format_diagnostic_output(data: Diagnostics, *, max_installed_packages: int | '-' * 80, f"sys.executable: {data['python_paths']['sys_executable']}", '\nsys.path:', + *[f' {path}' for path in data['python_paths']['sys_path']], ]) - for path in data['python_paths']['sys_path']: - lines.append(f' {path}') - """ Common installation locations from :func:`check_common_locations` """ lines.extend([ @@ -717,8 +766,8 @@ def format_diagnostic_output(data: Diagnostics, *, max_installed_packages: int | name_vers = f'{name_vers.ljust(col1_width)} {loc}' lines.append(f' {name_vers}') - if max_installed_packages and (len(packages) > max_installed_packages): - lines.append(f' ... and {len(packages) - max_installed_packages} more packages') + if max_installed_packages and (len(pkg_versions) > max_installed_packages): + lines.append(f' ... and {len(pkg_versions) - max_installed_packages} more packages') else: lines.append(f" Error: {packages.get('error', 'Unknown error')}") @@ -739,7 +788,7 @@ def format_diagnostic_output(data: Diagnostics, *, max_installed_packages: int | class Args(argparse.Namespace): """ - Annotated :class:`argparse.Namespace` for script command-line arguments + Annotated :class:`argparse.Namespace` for script command-line arguments. """ max_installed_packages: int | None # --max-installed-packages save: bool # -s/--save @@ -749,7 +798,7 @@ class Args(argparse.Namespace): def build_parser() -> argparse.ArgumentParser: """ - Build argument parser for script + Build argument parser for script. :return: :class:`argparse.ArgumentParser` """ @@ -759,50 +808,65 @@ def build_parser() -> argparse.ArgumentParser: add_help=False, ) - console_out_opts = parser.add_argument_group('Console output options') - console_out_opts.add_argument( - '--max-installed-packages', - metavar='NUM', - type=int, - help='Max number of installed python packages to list under "INSTALLED PACKAGES"', - ) - - file_out_opts = parser.add_argument_group('File output options') - file_out_opts.add_argument( - '-s', - '--save', - action='store_true', - help='Save output to file instead of printing', - ) - file_out_opts.add_argument( - '-j', - '--json', - action='store_true', - help='Output as JSON format', - ) - file_out_opts.add_argument( - '-o', - '--output', - type=Path, - metavar='FILEPATH', - help='Output file path (default: python_diagnostic_.txt)', - ) - - other_group = parser.add_argument_group('Other options') - other_group.add_argument( - '-h', - '--help', - action='help', - default=argparse.SUPPRESS, - help='Show help message and exit', - ) + def add_console_out_opts() -> None: + """ + Add console output options group to the argument parser. + """ + console_out_opts = parser.add_argument_group('Console output options') + console_out_opts.add_argument( + '--max-installed-packages', + metavar='NUM', + type=int, + help='Max number of installed python packages to list under "INSTALLED PACKAGES"', + ) + add_console_out_opts() + + def add_file_out_opts() -> None: + """ + Add file output options group to the argument parser. + """ + file_out_opts = parser.add_argument_group('File output options') + file_out_opts.add_argument( + '-s', + '--save', + action='store_true', + help='Save output to file instead of printing', + ) + file_out_opts.add_argument( + '-j', + '--json', + action='store_true', + help='Output as JSON format', + ) + file_out_opts.add_argument( + '-o', + '--output', + type=Path, + metavar='FILEPATH', + help='Output file path (default: python_diagnostic_.txt)', + ) + add_file_out_opts() + + def add_other_opts() -> None: + """ + Add miscellaneous options group to the argument parser. + """ + other_opts = parser.add_argument_group('Other options') + other_opts.add_argument( + '-h', + '--help', + action='help', + default=argparse.SUPPRESS, + help='Show help message and exit', + ) + add_other_opts() return parser def main(argv: Sequence[str] | None = None) -> int: """ - Main entry point + Main entry point. :param argv: argument list to parse, defaults to sys.argv[1:] :return: int script exit code @@ -829,7 +893,7 @@ def main(argv: Sequence[str] | None = None) -> int: output_path = Path(f'python_diagnostic_{timestamp}.{extension}') output_path.write_text(output) - print('') + print() print(f'Diagnostic report saved to: {output_path}', file=sys.stderr) print(f'File size: {output_path.stat().st_size} bytes', file=sys.stderr) else: diff --git a/scripts/env/venvshell.py b/scripts/env/venvshell.py index 8bd40a7..8cf30d8 100755 --- a/scripts/env/venvshell.py +++ b/scripts/env/venvshell.py @@ -16,6 +16,7 @@ import sys import tempfile import textwrap +from collections.abc import Sequence from pathlib import Path from typing import TYPE_CHECKING from typing import Any @@ -27,7 +28,6 @@ if TYPE_CHECKING: from collections.abc import Iterable - from collections.abc import Sequence # ==================== @@ -144,6 +144,37 @@ def exit(self) -> None: # ================== +ANSI_COLORS: dict[str, int] = { + 'black': 30, 'bright_black': 90, + 'red': 31, 'bright_red': 91, + 'green': 32, 'bright_green': 92, + 'yellow': 33, 'bright_yellow': 93, + 'blue': 34, 'bright_blue': 94, + 'magenta': 35, 'bright_magenta': 95, + 'cyan': 36, 'bright_cyan': 96, + 'white': 37, 'bright_white': 97, + 'reset': 39, +} +ANSI_RESET_ALL = '\033[0m' + + +def _interpret_color(_color: StyleColor, offset: int = 0) -> str: + """ + Resolve a color name, integer, or RGB tuple to an ANSI SGR parameter string. + + :param _color: color as a name string, 256-color int, or (r, g, b) tuple + :param offset: offset to add for background colors (10 for bg, 0 for fg) + :return: ANSI SGR parameter string (e.g. ``'38;5;196'``) + """ + if isinstance(_color, int): + return f'{38 + offset};5;{_color:d}' + if isinstance(_color, (tuple, list)): + r, g, b = _color + return f'{38 + offset};2;{r:d};{g:d};{b:d}' + _color = cast('str', _color) + return str(ANSI_COLORS[_color] + offset) + + def style( text: Any, *, @@ -172,36 +203,6 @@ def style( """ if os.environ.get('NO_COLOR') is not None: return str(text) - - _ansi_colors: dict[str, int] = { - 'black': 30, 'bright_black': 90, - 'red': 31, 'bright_red': 91, - 'green': 32, 'bright_green': 92, - 'yellow': 33, 'bright_yellow': 93, - 'blue': 34, 'bright_blue': 94, - 'magenta': 35, 'bright_magenta': 95, - 'cyan': 36, 'bright_cyan': 96, - 'white': 37, 'bright_white': 97, - 'reset': 39, - } - _ansi_reset_all = '\033[0m' - - def _interpret_color(_color: StyleColor, offset: int = 0) -> str: - """ - Resolve a color name, integer, or RGB tuple to an ANSI SGR parameter string. - - :param _color: color as a name string, 256-color int, or (r, g, b) tuple - :param offset: offset to add for background colors (10 for bg, 0 for fg) - :return: ANSI SGR parameter string (e.g. ``'38;5;196'``) - """ - if isinstance(_color, int): - return f'{38 + offset};5;{_color:d}' - if isinstance(_color, (tuple, list)): - r, g, b = _color - return f'{38 + offset};2;{r:d};{g:d};{b:d}' - _color = cast('str', _color) - return str(_ansi_colors[_color] + offset) - if not isinstance(text, str): text = str(text) @@ -227,7 +228,7 @@ def _interpret_color(_color: StyleColor, offset: int = 0) -> str: bits.append(text) if reset: - bits.append(_ansi_reset_all) + bits.append(ANSI_RESET_ALL) return ''.join(bits) @@ -298,7 +299,13 @@ def typestyle(val: Any, **opts: Any) -> str: return style(repr(val), fg='bright_cyan', bold=True, **opts) if isinstance(val, str): return style(repr(val), fg='green', **opts) - return str(val) + if isinstance(val, Sequence): + return ''.join([ + style('[', fg='bright_white'), + ', '.join(typestyle(item, **opts) for item in val), + style(']', fg='bright_white'), + ]) + return style(val, **opts) def dim_paren(s: str, *, fg: StyleColor | None = None) -> str: @@ -318,26 +325,35 @@ def dim_paren(s: str, *, fg: StyleColor | None = None) -> str: def annotated_opt_help( opt_help: str, - *, + *extra_help_lines: str, default: Any | None = None, default_fg: StyleColor | None = None, envvar: str | None = None, + extra_line: bool = True, ) -> str: """ Build a styled argparse help string with optional default value and environment variable annotations. - :param opt_help: base help text for the option + :param opt_help: core help text for the option (will be styled "bright_white") + :param extra_help_lines: additional help text lines (joined by newlines) for the option. Outputted with no styling + after `opt_help` and before `default`/`envvar` :param default: default value to display below the help text :param default_fg: explicit foreground color for the default value (overrides :func:`typestyle`) :param envvar: environment variable name that can override this option + :param extra_line: if True (default), extra blank line included at the end of the option help text (visually spaces + sequential options apart from each other) :return: multi-line styled help string with annotations appended """ help_lines: list[str] = style(opt_help, fg='bright_white').splitlines() + for extra_help_line in extra_help_lines: + help_lines.extend(extra_help_line.splitlines()) if default is not None: help_lines.append(f' default: {typestyle(default) if default_fg is None else style(default, fg=default_fg)}') if envvar is not None: help_lines.append(f' env var: {style(envvar, fg="cyan")}') - return '\n'.join([*help_lines, ' ']) + if extra_line: + help_lines.append(' ') + return '\n'.join(help_lines) def printf( @@ -1010,7 +1026,7 @@ def main(argv: list[str] | None = None) -> None: cwd = Path.cwd() printf(f'No virtual environment found in {pathstyle(cwd)}', error=True) printf(' '.join([ - style('Use', dim=True, fg='bright_white'), optstyle('--path PATH', dim=True), + style('Use', dim=True, fg='bright_white'), optstyle('--path PATH', dim=True), *([style('or', dim=True, fg='bright_white'), optstyle('--hatch ENV', dim=True)] if HATCH_ENABLED else []), style('to specify an environment', dim=True, fg='bright_white'), ]), file=sys.stderr, indent=10) diff --git a/scripts/install/README.md b/scripts/install/README.md index 7d58980..3468db4 100644 --- a/scripts/install/README.md +++ b/scripts/install/README.md @@ -4,9 +4,9 @@ Bulletproof installer scripts for bootstrapping development environments with `u ## Overview -These scripts provide robust, cross-platform installation of essential Python development tools with comprehensive -error handling, retry logic, and helpful troubleshooting guidance. Perfect for bootstrapping new systems or onboarding -new developers. +These scripts provide robust, cross-platform installation of essential Python development tools with comprehensive error +handling, retry logic, and helpful troubleshooting guidance. Perfect for bootstrapping new systems or onboarding new +developers. ## Scripts @@ -15,6 +15,7 @@ new developers. Installs [uv](https://docs.astral.sh/uv/) - the blazing-fast Python package manager and installer from Astral. **Features:** + - Cross-platform support (Linux, macOS, Windows) - Smart executable detection (checks PATH and common install locations) - Configurable network retry logic with exponential backoff @@ -24,6 +25,7 @@ Installs [uv](https://docs.astral.sh/uv/) - the blazing-fast Python package mana - Force reinstall option **Usage:** + ```bash # Simple installation ./scripts/install/install_uv.py @@ -45,12 +47,14 @@ Installs [uv](https://docs.astral.sh/uv/) - the blazing-fast Python package mana ``` **Options:** + - `--force` - Force reinstall even if UV is already installed - `-v, --verbose` - Enable verbose output for debugging - `--retries N` - Maximum number of download retry attempts (default: 3) - `--retry-delay SECONDS` - Initial delay in seconds between retries with exponential backoff (default: 2) **What it does:** + 1. Checks Python version (requires 3.8+) 2. Searches for existing `uv` installation in PATH and common locations 3. Downloads the official installer from https://astral.sh/uv/install.sh @@ -59,6 +63,7 @@ Installs [uv](https://docs.astral.sh/uv/) - the blazing-fast Python package mana 6. Cleans up temporary files **Exit codes:** + - `0` - Success or already installed - `2` - Download failed - `3` - Installation failed @@ -71,6 +76,7 @@ Installs [uv](https://docs.astral.sh/uv/) - the blazing-fast Python package mana Installs [Hatch](https://hatch.pypa.io/) - the modern Python project manager and build system. **Features:** + - Cross-platform support (Linux, macOS, Windows) - Uses official Hatch universal installer - Smart executable detection in multiple common locations @@ -80,6 +86,7 @@ Installs [Hatch](https://hatch.pypa.io/) - the modern Python project manager and - Verbose and force options **Usage:** + ```bash # Simple installation ./scripts/install/install_hatch.py @@ -101,12 +108,14 @@ Installs [Hatch](https://hatch.pypa.io/) - the modern Python project manager and ``` **Options:** + - `--force` - Force reinstall even if Hatch is already installed - `-v, --verbose` - Enable verbose output for debugging - `--retries N` - Maximum number of download retry attempts (default: 3) - `--retry-delay SECONDS` - Initial delay in seconds between retries with exponential backoff (default: 2) **What it does:** + 1. Validates Python version (3.8+ required) 2. Checks for existing Hatch installation 3. Downloads universal installer from GitHub releases @@ -115,6 +124,7 @@ Installs [Hatch](https://hatch.pypa.io/) - the modern Python project manager and 6. Provides shell-specific PATH setup instructions if needed **Exit codes:** + - `0` - Success or already installed - `2` - Download failed - `3` - Installation failed @@ -127,6 +137,7 @@ Installs [Hatch](https://hatch.pypa.io/) - the modern Python project manager and Both scripts share a robust architecture: ### Smart Detection + ```python # Checks PATH first $ which uv @@ -139,12 +150,14 @@ $ which uv ``` ### Network Resilience + - Configurable retry attempts (default: 3, customize with `--retries`) - Configurable initial delay with exponential backoff (default: 2s, customize with `--retry-delay`) - 30-second timeout per request - Helpful troubleshooting if all retries fail **Retry behavior:** + ```bash # Default: 3 attempts with 2s, 4s, 8s delays ./scripts/install/install_uv.py @@ -157,6 +170,7 @@ $ which uv ``` The exponential backoff formula is: `delay = retry_delay * (2 ** (attempt - 2))` + - Attempt 1: No delay - Attempt 2: Initial delay (e.g., 2s) - Attempt 3: 2× initial delay (e.g., 4s) @@ -164,7 +178,9 @@ The exponential backoff formula is: `delay = retry_delay * (2 ** (attempt - 2))` - And so on... ### Shell Intelligence + Detects your shell and provides appropriate PATH commands: + - **bash/zsh**: `export PATH="..."` - **fish**: `set -gx PATH "..." $PATH` - **tcsh/csh**: `setenv PATH "..."` @@ -172,10 +188,10 @@ Detects your shell and provides appropriate PATH commands: ### Platform Support | Platform | install_uv.py | install_hatch.py | -|----------|---------------|------------------| -| macOS | ✓ | ✓ | -| Linux | ✓ | ✓ | -| Windows | ✓ | ✓ | +| -------- | ------------- | ---------------- | +| macOS | ✓ | ✓ | +| Linux | ✓ | ✓ | +| Windows | ✓ | ✓ | ## Troubleshooting @@ -203,11 +219,13 @@ To make UV available globally, add it to your PATH: ### Download failures Scripts provide actionable troubleshooting: + 1. Check internet connection 2. Verify access to installer URLs 3. Check if proxy configuration is needed **For unreliable networks:** + ```bash # Increase retry attempts and delay ./scripts/install/install_uv.py --retries 10 --retry-delay 5 @@ -219,6 +237,7 @@ Scripts provide actionable troubleshooting: ### Verification failures If installation succeeds but verification fails: + 1. Close and reopen terminal 2. Try `--force` to reinstall 3. Check official documentation links diff --git a/scripts/install/install_hatch.py b/scripts/install/install_hatch.py index 2e73d28..84a89f2 100755 --- a/scripts/install/install_hatch.py +++ b/scripts/install/install_hatch.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Programmatically install Hatch using the official installer script +Programmatically install Hatch using the official installer script. """ from __future__ import annotations @@ -9,17 +9,28 @@ import functools import os import platform +import re +import shutil +import ssl import subprocess import sys import textwrap import time import urllib.error import urllib.request +from collections.abc import Sequence from pathlib import Path from typing import Any +from typing import Callable from typing import Protocol from typing import TypeVar from typing import Union +from typing import cast + +# ================================================================================================== +# Constants/defaults/globals +# ================================================================================================== + # Configuration MIN_PYTHON: tuple[int, int] = (3, 8) # Minimum supported python version. Script exits with error if not met @@ -35,17 +46,28 @@ VERBOSE: bool = False -PathLike = Union[Path, str] +# ================================================================================================== +# Types/protocols +# ================================================================================================== + + +PathLike = Union[Path, str] +StyleColor = Union[int, tuple[int, int, int], str] + +T_contra = TypeVar('T_contra', contravariant=True) -T_contra = TypeVar('T_contra', contravariant=True) class SupportsWrite(Protocol[T_contra]): - def write(self, s: T_contra, /) -> object: ... + """ + Protocol for writable things (like :attr:`sys.stdout` and :attr:`sys.stderr`). + """ + def write(self, s: T_contra, /) -> object: + ... class ExitCode(int, enum.Enum): """ - Script exit codes + Script exit codes. """ SUCCESS = 0 ALREADY_INSTALLED = 0 @@ -57,136 +79,418 @@ class ExitCode(int, enum.Enum): PYTHON_VERSION = 6 -def vprint(*args: Any, **kwargs: Any) -> None: +class DownloadMethod(str, enum.Enum): """ - Print only if verbose mode is enabled + Installer download methods, ordered by the priority in which they may be attempted. + + ``curl`` is preferred over ``urllib`` by default because curl uses the platform's native trust store + (e.g. SecureTransport on macOS) """ - if VERBOSE: - print(*args, file=kwargs.pop('file', sys.stderr), **kwargs) + CURL = 'curl' + URLLIB = 'urllib' + + def __str__(self) -> str: + return self.value + + +# Default ordered list of download methods to attempt. curl is tried before urllib (see :class:`DownloadMethod`) +DEFAULT_DOWNLOAD_METHODS: tuple[DownloadMethod, ...] = (DownloadMethod.CURL, DownloadMethod.URLLIB) + + +# ================================================================================================== +# Formatting/logging +# ================================================================================================== + +ANSI_COLORS: dict[str, int] = { + 'black': 30, 'bright_black': 90, + 'red': 31, 'bright_red': 91, + 'green': 32, 'bright_green': 92, + 'yellow': 33, 'bright_yellow': 93, + 'blue': 34, 'bright_blue': 94, + 'magenta': 35, 'bright_magenta': 95, + 'cyan': 36, 'bright_cyan': 96, + 'white': 37, 'bright_white': 97, + 'reset': 39, +} +ANSI_RESET_ALL = '\033[0m' -def indent(s: str, num: int) -> str: + +def _interpret_color(_color: StyleColor, offset: int = 0) -> str: """ - Indent string by given number of spaces + Resolve a color name, integer, or RGB tuple to an ANSI SGR parameter string. - :param s: string to indent - :param num: number of spaces to indent each line in string `s` with - :return: indented string + :param _color: color as a name string, 256-color int, or (r, g, b) tuple + :param offset: offset to add for background colors (10 for bg, 0 for fg) + :return: ANSI SGR parameter string (e.g. ``'38;5;196'``) """ - return textwrap.indent(s, ' ' * num) + if isinstance(_color, int): + return f'{38 + offset};5;{_color:d}' + if isinstance(_color, (tuple, list)): + r, g, b = _color + return f'{38 + offset};2;{r:d};{g:d};{b:d}' + _color = cast('str', _color) + return str(ANSI_COLORS[_color] + offset) -def check_python_version() -> bool: +def style( + text: Any, + *, + fg: StyleColor | None = None, + bg: StyleColor | None = None, + bold: bool | None = None, + dim: bool | None = None, + underline: bool | None = None, + overline: bool | None = None, + italic: bool | None = None, + blink: bool | None = None, + reverse: bool | None = None, + strikethrough: bool | None = None, + reset: bool = True, +) -> str: + """ + Style text with ANSI escape codes. + + :param text: the string to style with ansi codes + :param fg: foreground color + :param bg: background color + :param bold: enable or disable bold mode + :param dim: enable or disable dim mode + :param underline: enable or disable underline + :param overline: enable or disable overline + :param italic: enable or disable italic + :param blink: enable or disable blinking + :param reverse: enable or disable inverse rendering + :param strikethrough: enable or disable striking through text + :param reset: add a reset-all code at the end of the string + :return: styled text """ - Check minimum Python version requirement (:attr:`MAX_PYTHON` above) + if not isinstance(text, str): + text = str(text) - :return: True if minimum python version requirement met, False if not + bits: list[str] = [] + if fg: + try: + bits.append(f'\033[{_interpret_color(fg)}m') + except KeyError: + raise TypeError(f'Unknown color {fg!r}') from None + if bg: + try: + bits.append(f'\033[{_interpret_color(bg, 10)}m') + except KeyError: + raise TypeError(f'Unknown color {bg!r}') from None + if bold is not None: + bits.append(f'\033[{1 if bold else 22}m') + if dim is not None: + bits.append(f'\033[{2 if dim else 22}m') + if underline is not None: + bits.append(f'\033[{4 if underline else 24}m') + if overline is not None: + bits.append(f'\033[{53 if overline else 55}m') + if italic is not None: + bits.append(f'\033[{3 if italic else 23}m') + if blink is not None: + bits.append(f'\033[{5 if blink else 25}m') + if reverse is not None: + bits.append(f'\033[{7 if reverse else 27}m') + if strikethrough is not None: + bits.append(f'\033[{9 if strikethrough else 29}m') + + bits.append(text) + if reset: + bits.append(ANSI_RESET_ALL) + return ''.join(bits) + + +def unstyle(text: str) -> str: """ - if sys.version_info < MIN_PYTHON: - print( - f'❌ Error: ' - f'Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+ required, ' - f'but you have Python {sys.version_info.major}.{sys.version_info.minor}', - file=sys.stderr, - ) - return False - else: - vprint(f'✅ Python version {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}') - return True + Remove ANSI styling information from a string. + + :param text: the text to remove style information from + :return: string with ANSI styling characters removed + """ + return re.sub(r'\033\[[;?0-9]*[a-zA-Z]', '', text) -def detect_shell() -> str | None: +def folduser(path: PathLike) -> str: """ - Detect the user's shell + Does the opposite of :meth:`pathlib.Path.expanduser`, replacing the user's home directory with a "~". - :return: current shell + :param path: path + :return: str folded path """ - if shell := os.environ.get('SHELL', ''): - return Path(shell).name - else: - return None + home = str(Path.home()) + return str(path).replace(home, '~') -def get_shell_rc_file() -> Path | None: +def pathstyle(path: PathLike, **kwargs: Any) -> str: """ - Get the appropriate shell RC file path + Style a file path with magenta foreground and home directory condensed to ``~``. - :return: path to shell RC file for current shell + :param path: path to style + :return: styled path string """ - shell = detect_shell() - home = Path.home() + return style(folduser(path), fg='magenta', **kwargs) - shell_rc_map: dict[str, Path] = { - 'bash': home / '.bashrc', - 'zsh': home / '.zshrc', - 'fish': home / '.config' / 'fish' / 'config.fish', - 'tcsh': home / '.tcshrc', - 'csh': home / '.cshrc', - } - # For macOS, bash uses .bash_profile instead of .bashrc - if (shell == 'bash') and (platform.system() == 'Darwin'): - bash_profile = home / '.bash_profile' - if bash_profile.exists(): - return bash_profile +def optstyle(text: Any, **opts: Any) -> str: + """ + Style a CLI option name in yellow. - return shell_rc_map.get(shell) if shell else None + :param text: the option name to style + :param opts: additional keyword arguments forwarded to :func:`style` + :return: ANSI-styled string + """ + return style(text, fg='yellow', **opts) -def get_path_export_command(install_dir: Path) -> str | None: +def typestyle(val: Any, **opts: Any) -> str: """ - Get the command to add directory to PATH based on shell + Style a Python value by its type using semantic colors (roughly matching default repr styling provided by rich). + + Color mapping: - :param install_dir: directory path - :return: str shell command + - ``None`` = magenta italic + - ``bool`` = green (True) / red (False) italic + - ``Path`` = magenta with folduser + - ``int``/``float`` = bright cyan bold + - ``str`` = green + + Other types fall through to ``str(val)`` unstyled + + :param val: the Python value to style + :return: the ANSI-styled representation """ - shell = detect_shell() - if shell in ('bash', 'zsh'): - return f'export PATH="{install_dir}:$PATH"' - elif shell == 'fish': - return f'set -gx PATH "{install_dir}" $PATH' - elif shell in ('tcsh', 'csh'): - return f'setenv PATH "{install_dir}:$PATH"' - else: + if val is None: + return style(str(val), fg='magenta', italic=True, **opts) + if isinstance(val, bool): + return style(str(val), fg=f'bright_{"green" if val else "red"}', italic=True, **opts) + if isinstance(val, Path): + return style(folduser(val), fg='magenta', **opts) + if isinstance(val, (int, float)): + return style(repr(val), fg='bright_cyan', bold=True, **opts) + if isinstance(val, str): + return style(repr(val), fg='green', **opts) + if isinstance(val, Sequence): + return ''.join([ + style('[', fg='bright_white'), + ', '.join(typestyle(item, **opts) for item in val), + style(']', fg='bright_white'), + ]) + return style(val, **opts) + + +def annotated_opt_help( + opt_help: str, + *extra_help_lines: str, + choices: Sequence[Any] | None = None, + default: Any | None = None, + default_fg: StyleColor | None = None, + envvar: str | None = None, + extra_line: bool = True, +) -> str: + """ + Build a styled argparse help string with optional default value and environment variable annotations. + + :param opt_help: core help text for the option (will be styled "bright_white") + :param extra_help_lines: additional help text lines (joined by newlines) for the option. Outputted with no styling + after `opt_help` and before `default`/`envvar` + :param choices: valid option value choices to display below the help text + :param default: default value to display below the help text + :param default_fg: explicit foreground color for the default value (overrides :func:`typestyle`) + :param envvar: environment variable name that can override this option + :param extra_line: if True (default), extra blank line included at the end of the option help text (visually spaces + sequential options apart from each other) + :return: multi-line styled help string with annotations appended + """ + help_lines: list[str] = style(opt_help, fg='bright_white').splitlines() + for extra_help_line in extra_help_lines: + help_lines.extend(extra_help_line.splitlines()) + if choices is not None: + help_lines.append(f' choices: {typestyle(choices)}') + if default is not None: + help_lines.append(f' default: {typestyle(default) if default_fg is None else style(default, fg=default_fg)}') + if envvar is not None: + help_lines.append(f' env var: {style(envvar, fg="cyan")}') + if extra_line: + help_lines.append(' ') + return '\n'.join(help_lines) + + +def printf( + text: str = '', + *, + fg: StyleColor | None = None, + bold: bool | None = None, + dim: bool | None = None, + indent: int = 0, + file: SupportsWrite[str] | None = None, + verbose: bool = False, +) -> None: + """ + Print styled text with optional indent and verbose gating. + + :param text: text to print + :param fg: foreground color + :param bold: bold mode + :param dim: dim mode + :param indent: number of spaces to indent output by + :param file: output file (defaults to stdout, or stderr for verbose output) + :param verbose: only print when global ``VERBOSE`` is True. Output is dim and sent to stderr + """ + if verbose and not VERBOSE: + return + if verbose: + fg = fg or 'bright_white' + dim = True if dim is None else dim + file = file or sys.stderr + + print( + style(textwrap.indent(text, ' ' * indent), fg=fg, bold=bold, dim=dim), + file=file, + ) + + +# ================================================================================================== +# Python checks +# ================================================================================================== + + +def check_python_version() -> bool: + """ + Check minimum Python version requirement (:attr:`MIN_PYTHON` above). + + :return: True if minimum python version requirement met, False if not + """ + if sys.version_info < MIN_PYTHON: + printf( + f'❌ Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+ required, ' + f'but you have Python {sys.version_info.major}.{sys.version_info.minor}', + fg='bright_red', + file=sys.stderr, + ) + return False + vers_str = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}' + printf(f'✅ Python version {vers_str}', verbose=True) + return True + + +# ================================================================================================== +# Shell detection & path mapping +# ================================================================================================== + + +class Shell(enum.Enum): + """ + Known shells with their associated RC file paths and PATH export syntax. + """ + BASH = 'bash' + ZSH = 'zsh' + FISH = 'fish' + TCSH = 'tcsh' + CSH = 'csh' + + @classmethod + def detect(cls) -> Shell | None: + """ + Detect the user's shell from the ``$SHELL`` environment variable. + + :return: detected :class:`Shell` member, or ``None`` if unrecognized or unset + """ + if shell := os.environ.get('SHELL', ''): + name = Path(shell).name + try: + return cls(name) + except ValueError: + return None return None + @property + def rc_file(self) -> Path: + """ + Get the appropriate RC file path for this shell. + + On macOS, bash prefers ``~/.bash_profile`` over ``~/.bashrc`` when the file exists. + + :return: path to the shell's RC file + """ + home = Path.home() + rc_map: dict[Shell, Path] = { + Shell.BASH: home / '.bashrc', + Shell.ZSH: home / '.zshrc', + Shell.FISH: home / '.config' / 'fish' / 'config.fish', + Shell.TCSH: home / '.tcshrc', + Shell.CSH: home / '.cshrc', + } + if (self is Shell.BASH) and (platform.system() == 'Darwin'): + bash_profile = home / '.bash_profile' + if bash_profile.exists(): + return bash_profile + return rc_map[self] + + def path_export_command(self, directory: PathLike) -> str: + """ + Get the command to add a directory to ``PATH`` for this shell. + + :param directory: directory to prepend to ``PATH`` + :return: shell-specific export command string + """ + directory_path = Path(directory).expanduser().resolve() + if self in (Shell.BASH, Shell.ZSH): + return f'export PATH="{directory_path}:$PATH"' + if self is Shell.FISH: + return f'set -gx PATH "{directory_path}" $PATH' + # TCSH, CSH + return f'setenv PATH "{directory_path}:$PATH"' + + +# ================================================================================================== +# Local executable checks +# ================================================================================================== + # TODO: support brew install location as well # TODO: allow installation from current environment as well (from `pip install`) def get_common_install_locations() -> list[Path]: """ - Get common installation locations for Hatch based on platform + Get common installation locations for Hatch on macOS. :return: list of filepaths (:class:`pathlib.Path`) """ - home = Path.home() - system = platform.system().lower() - - locations: list[Path] = [] - if system in ('linux', 'darwin'): - locations = [ - home / '.local' / 'bin' / 'hatch', - home / '.cargo' / 'bin' / 'hatch', - Path('/usr/local/bin/hatch'), - Path('/usr/bin/hatch'), - home / '.hatch' / 'bin' / 'hatch', - ] - elif system == 'windows': - locations = [ - home / '.local' / 'bin' / 'hatch.exe', - home / '.cargo' / 'bin' / 'hatch.exe', - home / 'AppData' / 'Local' / 'hatch' / 'hatch.exe', - Path('C:/Program Files/hatch/hatch.exe'), - ] + home = Path.home() + locations: list[Path] = [ + home / '.local' / 'bin' / 'hatch', + home / '.cargo' / 'bin' / 'hatch', + Path('/usr/local/bin/hatch'), + Path('/usr/local/hatch/bin/hatch'), + Path('/usr/bin/hatch'), + home / '.hatch' / 'bin' / 'hatch', + ] + + # macOS pip ``--user`` installs land in ``~/Library/Python//bin/hatch`` (one entry per framework Python). + # Scanning catches hatch installed by ``install_via_pip`` even when the system python3 falls back to PEP 370 + # user-base, plus any pre-existing pip --user installs. + library_python = home / 'Library' / 'Python' + if library_python.is_dir(): + for child in sorted(library_python.iterdir()): + hatch_path = child / 'bin' / 'hatch' + if hatch_path not in locations: + locations.append(hatch_path) + + # Locations registered dynamically by ``install_via_pip`` (e.g. system python3 scripts dir) + for path in PIP_INSTALL_LOCATIONS: + if path not in locations: + locations.append(path) + return locations def find_executable(*, path_only: bool = False) -> tuple[str | None, str | None]: """ - Try to find the Hatch executable in common locations + Try to find the Hatch executable in common locations. :param path_only: if True, only look for executable in PATH. If False, look for executable in other common locations - where UV might be installed + where hatch might be installed :return: tuple (executable path, version) """ @@ -224,7 +528,7 @@ def find_executable(*, path_only: bool = False) -> tuple[str | None, str | None] def is_installed(*, quiet: bool = False) -> bool: """ - Check if Hatch is already installed + Check if Hatch is already installed. :param quiet: suppress console output if True :return: True if `hatch` already installed, False if not @@ -232,32 +536,62 @@ def is_installed(*, quiet: bool = False) -> bool: executable, version = find_executable() if executable: if not quiet: - print(f'✅ Hatch is already installed: {version}') + printf(f'✅ Already installed: {style(version, fg="bright_white")}', fg='bright_green') if executable != 'hatch': - print(f' 📍 Found at: {executable}') - print(' 💡 Note: Consider adding this location to your PATH') + printf(f'Found at: {pathstyle(executable)}', indent=3) + printf( + style('NOTE: ', fg='magenta', bold=True) + + style('consider adding this to your PATH', fg='bright_white'), + indent=3, + ) return True - else: - return False + return False + + +# ================================================================================================== +# Installer retrieval/execution +# ================================================================================================== def get_installer_url() -> str: """ - Get the appropriate Hatch installer URL based on the platform + Get the Hatch installer URL for macOS. - :raises OSError: if current platform/operating system is not supported + On macOS, Hatch is distributed as a ``.pkg`` installer. + + :raises OSError: if current platform is not macOS :return: installer URL """ - system = platform.system().lower() - if system in ('linux', 'darwin'): - return 'https://github.com/pypa/hatch/releases/latest/download/hatch-universal-installer.py' - elif system == 'windows': - return 'https://github.com/pypa/hatch/releases/latest/download/hatch-universal-installer.py' + if platform.system().lower() != 'darwin': + raise OSError(f'This installer only supports macOS (got: {platform.system()})') + return 'https://github.com/pypa/hatch/releases/latest/download/hatch-universal.pkg' + + +def write_installer(dest: PathLike, content: bytes) -> bool: + """ + Write downloaded installer bytes to disk, surfacing helpful troubleshooting tips on failure. + + :param dest: destination file path + :param content: bytes to write + :return: True if successful, False if not + """ + try: + with Path(dest).expanduser().open('wb') as f: + f.write(content) + except (PermissionError, OSError) as e: + printf(f'❌ Failed to write installer to disk: {e!s}', fg='bright_red', file=sys.stderr) + print(file=sys.stderr) + printf('🔧 Troubleshooting:', fg='bright_white', file=sys.stderr) + printf(f'1. Check write permissions for: {pathstyle(dest)}', indent=3, file=sys.stderr) + printf('2. Verify sufficient disk space is available', indent=3, file=sys.stderr) + printf('3. Ensure the parent directory exists', indent=3, file=sys.stderr) + return False else: - raise OSError(f'Unsupported operating system: {system}') + printf(f'✅ Downloaded to {pathstyle(dest)}', fg='bright_green') + return True -def download_installer( +def download_with_urllib( url: str, dest: PathLike, *, @@ -266,262 +600,547 @@ def download_installer( retry_delay: float = DOWNLOAD_RETRY_DELAY, ) -> bool: """ - Download the installer script with retry logic + Download a file using Python's :mod:`urllib`. - :param url: installer URL - :param dest: where to download installer to - :param timeout: download timeout in seconds - :param retries: maximum number of download retry attempts + Aborts early on SSL verification errors so the curl fallback can be tried sooner. + SSL errors are not transient and additional retries will not recover. + + :param url: source URL + :param dest: destination file path + :param timeout: per-request timeout in seconds + :param retries: maximum number of retry attempts :param retry_delay: initial delay in seconds between retries, before exponential backoff :return: True if successful, False if not """ + content: bytes | None = None for attempt in range(1, retries + 1): try: if attempt > 1: delay = retry_delay * (2 ** (attempt - 2)) # Exponential backoff - print(f'⏱️ Retrying in {delay} seconds... (attempt {attempt}/{retries})') + printf(f'⏱️ Retrying in {delay} seconds... (attempt {attempt}/{retries})', fg='bright_yellow') time.sleep(delay) - print(f'⏬ Downloading installer from {url}...') - vprint(f' Attempt {attempt}/{retries}') + printf(f'⏬ Downloading installer from {url} (urllib)...', bold=True) + printf(f'Attempt {attempt}/{retries}', indent=3, verbose=True) - with urllib.request.urlopen(url, timeout=timeout) as response: + with urllib.request.urlopen(url, timeout=timeout) as response: # noqa: S310 content = response.read() - with Path(dest).expanduser().open('wb') as f: - f.write(content) except urllib.error.URLError as e: - print(f'❌ Download failed: {e}', file=sys.stderr) + printf(f'❌ urllib download failed: {e!s}', fg='bright_red', file=sys.stderr) + if isinstance(e.reason, ssl.SSLError): + printf('SSL verification failed; aborting urllib retries', indent=3, verbose=True, file=sys.stderr) + return False if attempt == retries: - print('', file=sys.stderr) - print('🔧 Troubleshooting:', file=sys.stderr) - print(' 1. Check your internet connection', file=sys.stderr) - print(' 2. Verify you can access https://github.com', file=sys.stderr) - print(' 3. Check if a proxy is required in your network', file=sys.stderr) return False except Exception as e: - print(f'❌ Unexpected error during download: {e}', file=sys.stderr) + printf(f'❌ Unexpected error during urllib download: {e!s}', fg='bright_red', file=sys.stderr) if attempt == retries: return False + # Download succeeded, break out of retry loop + else: + break + + if content is None: + return False + return write_installer(dest, content) + + +def download_with_curl( + url: str, + dest: PathLike, + *, + timeout: float = DOWNLOAD_TIMEOUT, + retries: int = DOWNLOAD_RETRIES, + retry_delay: float = DOWNLOAD_RETRY_DELAY, +) -> bool: + """ + Download a file using the system ``curl`` executable. + + Useful as a fallback when :mod:`urllib` fails — particularly with SSL certificate verification errors — since curl + uses the platform's native trust store (e.g. SecureTransport on macOS) + + :param url: source URL + :param dest: destination file path + :param timeout: per-request timeout in seconds + :param retries: maximum number of retry attempts + :param retry_delay: initial delay in seconds between retries, before exponential backoff + :return: True if successful, False if not + """ + curl = shutil.which('curl') + if curl is None: + printf('curl not available, skipping curl fallback', verbose=True, file=sys.stderr) + return False + + dest_path = Path(dest).expanduser() + cmd: list[str] = [ + curl, + '--fail', + '--silent', + '--show-error', + '--location', + '--max-time', str(int(timeout)), + '--output', str(dest_path), + url, + ] + + for attempt in range(1, retries + 1): + if attempt > 1: + delay = retry_delay * (2 ** (attempt - 2)) # Exponential backoff + printf(f'⏱️ Retrying in {delay} seconds... (attempt {attempt}/{retries})', fg='bright_yellow') + time.sleep(delay) + + printf(f'⏬ Downloading installer from {url} (curl)...', bold=True) + printf(f'Attempt {attempt}/{retries}', indent=3, verbose=True) + printf(f'Running: {" ".join(cmd)}', indent=3, verbose=True) + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + err = (e.stderr or '').strip() or str(e) + printf(f'❌ curl download failed: {err}', fg='bright_red', file=sys.stderr) + if attempt == retries: + return False else: - print(f'✅ Downloaded to {dest}') + printf(f'✅ Downloaded to {pathstyle(dest_path)}', fg='bright_green') return True return False +# Dispatch table mapping each download method to its implementation. +# Both functions share the same signature ``(url, dest, *, timeout, retries, retry_delay) -> bool`` +DOWNLOAD_FUNCS: dict[DownloadMethod, Callable[..., bool]] = { + DownloadMethod.CURL: download_with_curl, + DownloadMethod.URLLIB: download_with_urllib, +} + + +def download_installer( + url: str, + dest: PathLike, + *, + methods: Sequence[DownloadMethod] = DEFAULT_DOWNLOAD_METHODS, + timeout: float = DOWNLOAD_TIMEOUT, + retries: int = DOWNLOAD_RETRIES, + retry_delay: float = DOWNLOAD_RETRY_DELAY, +) -> bool: + """ + Download the installer script, trying each configured download method in order until one succeeds. + + The default order (see :attr:`DEFAULT_DOWNLOAD_METHODS`) tries ``curl`` before :mod:`urllib`. curl handles + environments where Python's bundled OpenSSL CA list cannot validate the TLS chain to ``url`` + + :param url: installer URL + :param dest: where to download installer to + :param methods: ordered download methods to attempt; each is tried until one succeeds + :param timeout: download timeout in seconds + :param retries: maximum number of download retry attempts + :param retry_delay: initial delay in seconds between retries, before exponential backoff + :raises ValueError: if `url` does not start with "http:" or "https:" + :return: True if successful, False if not + """ + if not url.startswith(('http:', 'https:')): + raise ValueError("URL must start with 'http:' or 'https:'") + + ordered_methods = list(methods) or list(DEFAULT_DOWNLOAD_METHODS) + for index, method in enumerate(ordered_methods): + download_func = DOWNLOAD_FUNCS[method] + if download_func(url, dest, timeout=timeout, retries=retries, retry_delay=retry_delay): + return True + + # Announce the fallback to the next method, if any remain + if index < len(ordered_methods) - 1: + next_method = ordered_methods[index + 1] + printf(f'⚠️ {method} download failed; falling back to {next_method}', fg='bright_yellow') + printf('') + + return False + + def run_installer(installer_path: PathLike) -> bool: """ - Execute the Hatch installer script + Execute the Hatch ``.pkg`` installer via ``sudo installer``. - :param installer_path: path to installer script + :param installer_path: path to ``.pkg`` installer :return: True on success, False on failure """ - print('💿 Running Hatch installer...') + printf('💿 Running Hatch .pkg installer (requires sudo)...', bold=True) + try: + result = subprocess.run( + ['sudo', 'installer', '-pkg', str(installer_path), '-target', '/'], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + printf(f'❌ Installation failed: {e!s}', fg='bright_red', file=sys.stderr) + printf(e.stderr, indent=3, file=sys.stderr) + return False + else: + printf(result.stdout, indent=3) + return True + + +# ================================================================================================== +# `pip`-based installation (last-resort fallback) +# ================================================================================================== + + +# Locations registered dynamically by :func:`install_via_pip` so :func:`find_executable` can find hatch after a +# successful pip install on this run. +PIP_INSTALL_LOCATIONS: list[Path] = [] + + +def resolve_system_python() -> str | None: + """ + Locate a system ``python3`` executable suitable for the pip-install fallback. + + Prefers the macOS system interpreter at ``/usr/bin/python3``, then anything named ``python3`` on ``PATH``. + + :return: absolute path to a python3 executable, or ``None`` if none can be found + """ + system_python = Path('/usr/bin/python3') + if system_python.exists(): + return str(system_python) + return shutil.which('python3') + + +def register_pip_install_location(python_exe: str) -> None: + """ + Register the scripts directory of ``python_exe`` so :func:`find_executable` can find ``hatch``. + + Asks the interpreter for its ``sysconfig`` scripts path and appends ``/hatch`` to the module-level + :attr:`PIP_INSTALL_LOCATIONS` list. + + :param python_exe: path to a python interpreter that just ran ``pip install hatch`` + """ try: result = subprocess.run( - [sys.executable, str(installer_path)], + [python_exe, '-c', 'import sysconfig; print(sysconfig.get_path("scripts"))'], check=True, capture_output=True, text=True, ) + except (subprocess.CalledProcessError, FileNotFoundError): + return + else: + candidate = Path(result.stdout.strip()) / 'hatch' + if candidate not in PIP_INSTALL_LOCATIONS: + PIP_INSTALL_LOCATIONS.append(candidate) + printf(f'Registered pip install location: {pathstyle(candidate)}', indent=3, verbose=True) + + +def install_via_pip() -> bool: + """ + Install Hatch via ``python3 -m pip install hatch`` using the system ``python3`` executable. + + Used as a last-resort fallback when neither :mod:`urllib` nor ``curl`` could fetch the official ``.pkg`` installer + (or when the user cannot run ``sudo`` to execute the ``.pkg``). Installs into the system Python's site-packages so + the resulting ``hatch`` executable lands in that interpreter's scripts directory. + + :return: True on success, False on failure + """ + printf('') + printf('💿 Falling back to pip install hatch (system python3)...', bold=True) + + python3 = resolve_system_python() + if python3 is None: + printf('❌ Could not find a python3 executable for pip fallback', fg='bright_red', file=sys.stderr) + return False + printf(f'Using python: {pathstyle(python3)}', indent=3, verbose=True) + + cmd: list[str] = [python3, '-m', 'pip', 'install', 'hatch'] + printf(f'Running: {" ".join(cmd)}', indent=3, verbose=True) + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: - print(f'❌ Installation failed: {e}', file=sys.stderr) - print(indent(e.stderr, 3), file=sys.stderr) + printf(f'❌ pip install hatch failed (exit {e.returncode})', fg='bright_red', file=sys.stderr) + if e.stdout: + printf(e.stdout.rstrip(), indent=3, file=sys.stderr) + if e.stderr: + printf(e.stderr.rstrip(), indent=3, file=sys.stderr) return False + + except FileNotFoundError as e: + printf(f'❌ pip install hatch failed: {e!s}', fg='bright_red', file=sys.stderr) + return False + else: - print(indent(result.stdout, 3)) + if VERBOSE and result.stdout: + printf(result.stdout.rstrip(), indent=3, verbose=True) + printf('✅ Installed hatch via pip', fg='bright_green') + register_pip_install_location(python3) return True def verify_installation() -> bool: """ - Verify that Hatch was installed successfully. Provides detailed guidance if tool is installed but not in PATH + Verify that Hatch was installed successfully. Provides detailed guidance if tool is installed but not in PATH. :return: True on success, False on failure """ - print('') - print('🔍 Verifying installation...') - vprint('Checking all known installation locations...') + printf('') + printf('🔍 Verifying installation...', bold=True) + printf('Checking all known installation locations...', indent=3, verbose=True) # Use find_executable to check all locations executable, version = find_executable() if not executable: - print('❌ Hatch installation could not be verified\n', file=sys.stderr) - print('🔧 Troubleshooting:', file=sys.stderr) - print(' 1. Close and reopen your terminal', file=sys.stderr) - print(' 2. Try running the installer again with --force', file=sys.stderr) - print(' 3. Check the official Hatch installation guide:', file=sys.stderr) - print(f' {INSTALLATION_GUIDE_URL}', file=sys.stderr) + printf('❌ Hatch installation could not be verified\n', fg='bright_red', file=sys.stderr) + printf('🔧 Troubleshooting:', fg='bright_white', file=sys.stderr) + printf('1. Close and reopen your terminal', indent=3, file=sys.stderr) + printf(f'2. Try running the installer again with {optstyle("--force")}', indent=3, file=sys.stderr) + printf('3. Check the official Hatch installation guide:', indent=3, file=sys.stderr) + printf(INSTALLATION_GUIDE_URL, dim=True, indent=6, file=sys.stderr) return False # Tool found! - print(f'✅ Hatch installed successfully: {version}') + printf('✅ Installed successfully', fg='bright_green') + printf(version or '', indent=3) # Check if it's in PATH if executable == 'hatch': - print('✅ Hatch is in your PATH and ready to use') - vprint(f' Executable: {executable}') + printf('✅ In your PATH and ready to use', fg='bright_green') + printf(f'Executable: {executable}', indent=3, verbose=True) return True # Tool is installed but not in PATH + printf(f'📍 Found at: {pathstyle(executable)}\n') + printf('⚠️ Installed but not in your PATH', fg='bright_yellow') + + install_dir = Path(executable).parent + shell = Shell.detect() + + printf('') + printf('💡 To make it available globally, add it to your PATH:\n', fg='bright_white') + if shell: + rc_file = shell.rc_file + export_cmd = shell.path_export_command(install_dir) + printf(f'For {shell.value}, add this line to {pathstyle(rc_file)}:', indent=3) + printf(style(export_cmd, fg='bright_magenta'), indent=3) + printf('') + printf('Then run:', indent=3) + printf(style(f'source {folduser(rc_file)}', fg='bright_magenta'), indent=3) else: - print(f'📍 Found at: {executable}\n') - print('⚠️ Hatch is installed but not in your PATH') - - install_dir = Path(executable).parent - shell = detect_shell() - rc_file = get_shell_rc_file() - export_cmd = get_path_export_command(install_dir) - - print('') - print('💡 To make Hatch available globally, add it to your PATH:\n') - if rc_file and export_cmd and shell: - print(f' For {shell}, add this line to {rc_file}:') - print(f' {export_cmd}\n') - print(' Then run:') - print(f' source {rc_file}') - else: - print(f' Add {install_dir} to your PATH environment variable') - print(" Consult your shell's documentation for instructions") + printf(f'Add {pathstyle(install_dir)} to your PATH environment variable', indent=3) + printf("Consult your shell's documentation for instructions", indent=3, dim=True) - print('') - print(f' Or use the full path: {executable}') + printf('') + printf(f'Or use the full path: {pathstyle(executable)}', indent=3) - # Still consider this a success since the tool is installed - return True + # Still consider this a success since the tool is installed + return True + + +# ================================================================================================== +# Command-line arguments +# ================================================================================================== + + +class Args(argparse.Namespace): + """ + Annotated :class:`argparse.Namespace` returned by :func:`parse_args`. + """ + force: bool # -f/--force + ensure: bool # -e/--ensure + verbose: bool # -v/--verbose + timeout: float # -t/--timeout + retries: int # -r/--retries + retry_delay: float # --retry-delay + download_methods: list[DownloadMethod] # -m/--download-method -def parse_args(argv: list[str] | None = None) -> argparse.Namespace: +def parse_args(argv: Sequence[str] | None = None) -> Args: """ - Build parser and parse command-line arguments + Build parser and parse command-line arguments. - :param argv: argument list of parse, defaults to ``sys.argv[1:]`` - :return: :class:`argparse.Namespace` with parsed argument values + :param argv: argument list of parse. Defaults to ``sys.argv[1:]`` + :return: annotated namespace of parsed args (:class:`Args`) """ global VERBOSE + PROG = style('%(prog)s', fg='bright_magenta') + + def dim(text: Any, **opts: Any) -> str: + return style(text, dim=opts.pop('dim', True), **opts) + # Build argument parser parser = argparse.ArgumentParser( - description='Install Hatch build tool using official installer script', + description=style('Install Hatch build tool using official installer script', fg='bright_yellow'), formatter_class=functools.partial( - argparse.RawDescriptionHelpFormatter, - max_help_position=50, + argparse.RawTextHelpFormatter, + max_help_position=80, ), epilog='\n'.join([ - 'Examples:', + style('Examples:', fg='bright_blue', bold=True), '', - ' %(prog)s # Normal installation', - ' %(prog)s --force # Force reinstall even if present', - ' %(prog)s --verbose # Show detailed progress', - ' %(prog)s --force -v # Force reinstall with verbose output', - ' %(prog)s --retries 5 # Increase download retry attempts to 5', - ' %(prog)s --retry-delay 3 # Set initial retry delay to 3 seconds', - ' %(prog)s --retries 5 --retry-delay 3 # Custom retry configuration', + f' {PROG} {dim("# Normal installation")}', + f' {PROG} --force {dim("# Force reinstall even if present")}', + f' {PROG} --verbose {dim("# Show detailed progress")}', + f' {PROG} --force -v {dim("# Force reinstall with verbose output")}', + f' {PROG} --retries 5 {dim("# Increase download retry attempts to 5")}', + f' {PROG} --retry-delay 3 {dim("# Set initial retry delay to 3 seconds")}', + f' {PROG} --retries 5 --retry-delay 3 {dim("# Custom retry configuration")}', + f' {PROG} --download-method curl {dim("# Only use curl to download the installer")}', + f' {PROG} --download-method urllib curl {dim("# Try urllib first, then fall back to curl")}', ]), add_help=False, ) - execution_opts = parser.add_argument_group('Execution options') - execution_opts.add_argument( - '-f', - '--force', - action='store_true', - help='Force installation even if Hatch is already installed', - ) - execution_opts.add_argument( - '-e', - '--ensure', - action='store_true', - help='Install Hatch if it does not exist, otherwise quietly exit with no console output', - ) - - download_opts = parser.add_argument_group('Download options') - download_opts.add_argument( - '-t', - '--timeout', - type=float, - default=DOWNLOAD_TIMEOUT, - metavar='SECONDS', - help=f'Download timeout in seconds (default: {DOWNLOAD_TIMEOUT})', - ) - download_opts.add_argument( - '-r', - '--retries', - type=int, - default=DOWNLOAD_RETRIES, - metavar='N', - help=f'Maximum number of download retry attempts (default: {DOWNLOAD_RETRIES})', - ) - download_opts.add_argument( - '--retry-delay', - type=float, - default=DOWNLOAD_RETRY_DELAY, - metavar='SECONDS', - help=f'Initial delay in seconds between retries, uses exponential backoff (default: {DOWNLOAD_RETRY_DELAY})', - ) - - logging_opts = parser.add_argument_group('Logging options') - logging_opts.add_argument( - '-v', - '--verbose', - action='store_true', - help='Enable verbose output for debugging', - ) - - other_opts = parser.add_argument_group('Other options') - other_opts.add_argument( - '-h', - '--help', - action='help', - default=argparse.SUPPRESS, - help='Show this help message and exit', - ) + def add_execution_opts() -> None: + """ + Add execution option arguments (``--force``, ``--ensure``) to the parser. + """ + execution_opts = parser.add_argument_group(style('Execution options', fg='bright_blue', bold=True)) + execution_opts.add_argument( + '-f', + '--force', + action='store_true', + help=annotated_opt_help('Force installation even if Hatch is already installed'), + ) + execution_opts.add_argument( + '-e', + '--ensure', + action='store_true', + help=annotated_opt_help( + 'Install Hatch if it does not exist, otherwise quietly exit with no console output' + ), + ) + add_execution_opts() + + def add_download_opts() -> None: + """ + Add download option arguments (``--timeout``, ``--retries``, ``--retry-delay``) to the parser. + """ + download_opts = parser.add_argument_group(style('Download options', fg='bright_blue', bold=True)) + download_opts.add_argument( + '-t', + '--timeout', + type=float, + default=DOWNLOAD_TIMEOUT, + metavar='SECONDS', + help=annotated_opt_help( + 'Download timeout in seconds', + default=DOWNLOAD_TIMEOUT, + ), + ) + download_opts.add_argument( + '-r', + '--retries', + type=int, + default=DOWNLOAD_RETRIES, + metavar='NUM', + help=annotated_opt_help( + 'Maximum number of download retry attempts', + default=DOWNLOAD_RETRIES, + ), + ) + download_opts.add_argument( + '-d', + '--retry-delay', + type=float, + default=DOWNLOAD_RETRY_DELAY, + metavar='SECONDS', + help=annotated_opt_help( + 'Initial delay in seconds between retries, uses exponential backoff', + default=DOWNLOAD_RETRY_DELAY, + ), + ) + download_opts.add_argument( + '-m', + '--download-method', + dest='download_methods', + nargs='+', + type=DownloadMethod, + choices=list(DownloadMethod), + default=list(DEFAULT_DOWNLOAD_METHODS), + metavar='METHOD', + help=annotated_opt_help( + 'Ordered download methods to try, highest priority first', + choices=[m.value for m in DownloadMethod], + default=[m.value for m in DEFAULT_DOWNLOAD_METHODS], + ), + ) + add_download_opts() + + def add_other_opts() -> None: + """ + Add miscellaneous option arguments (``--help``) to the parser. + """ + other_opts = parser.add_argument_group(style('Other options', fg='bright_blue', bold=True)) + other_opts.add_argument( + '-v', + '--verbose', + action='store_true', + help='Enable verbose output for debugging', + ) + other_opts.add_argument( + '-h', + '--help', + action='help', + default=argparse.SUPPRESS, + help='Show this help message and exit', + ) + add_other_opts() # Parse arguments - args = parser.parse_args(argv) + args = parser.parse_args(argv, namespace=Args()) # Set global config variables with parsed command-line option values VERBOSE = args.verbose # Validate retry parameters if args.timeout < 0: - print('❌ Error: --timeout cannot be negative', file=sys.stderr) + printf(f'❌ {optstyle("--timeout")} cannot be negative', fg='bright_red', file=sys.stderr) sys.exit(ExitCode.INVALID_PARAMETERS) if args.retries < 1: - print('❌ Error: --retries must be at least 1', file=sys.stderr) + printf(f'❌ {optstyle("--retries")} must be at least 1', fg='bright_red', file=sys.stderr) sys.exit(ExitCode.INVALID_PARAMETERS) if args.retry_delay < 0: - print('❌ Error: --retry-delay cannot be negative', file=sys.stderr) + printf(f'❌ {optstyle("--retry-delay")} cannot be negative', fg='bright_red', file=sys.stderr) sys.exit(ExitCode.INVALID_PARAMETERS) # Enforce mutual exclusivity of --force and --ensure # Using ``add_mutually_exclusive_group()`` prevents us from adding a help text title to the execution options group if args.force and args.ensure: - print('❌ Error: --force and --ensure are mutually exclusive', file=sys.stderr) + printf( + f'❌ {optstyle("--force")} and {optstyle("--ensure")} are mutually exclusive', + fg='bright_red', + file=sys.stderr, + ) sys.exit(ExitCode.INVALID_PARAMETERS) return args -def main(argv: list[str] | None = None) -> None: +# ================================================================================================== +# Core script entry point +# ================================================================================================== + + +def main(argv: Sequence[str] | None = None) -> None: """ - Programmatically install Hatch using the official installer script + Programmatically install Hatch using the official installer script. - :param argv: argument list of parse, defaults to ``sys.argv[1:]`` + :param argv: argument list of parse. Defaults to ``sys.argv[1:]`` """ # Parse command-line arguments - args = parse_args(argv=argv) + args = parse_args(argv) if args.verbose or not args.ensure: - print('╔═══════════════════════════════╗') - print('║ Hatch Installation Script ║') - print('╚═══════════════════════════════╝') - print() - vprint(f'Download retry configuration: {args.retries} attempts, {args.retry_delay}s initial delay\n') + printf() + printf('Hatch installation', fg='bright_cyan', bold=True) + printf('─' * 77, dim=True) + printf( + f'Download configuration: methods={" → ".join(str(m) for m in args.download_methods)}, ' + f'{args.retries} attempts, {args.retry_delay}s initial delay\n', + verbose=True, + ) # Check Python version if not check_python_version(): @@ -531,52 +1150,74 @@ def main(argv: list[str] | None = None) -> None: if not args.force: if is_installed(quiet=args.ensure and not args.verbose): if not args.ensure: - print('⏭️ Skipping installation\n') - print('💡 Tip: Use --force to reinstall') + printf('⏭️ Skipping installation') + printf( + f'{style("Use", fg="bright_white")} {optstyle("--force")} ' + f'{style("to reinstall", fg="bright_white")}', + indent=3, + ) sys.exit(ExitCode.ALREADY_INSTALLED) else: - print('🔄 --force specified, proceeding with installation...\n') + printf(f'🔄 {optstyle("--force")} specified, proceeding with installation...\n', fg='bright_cyan') # Get installer URL try: url = get_installer_url() - vprint(f'Installer URL: {url}') + printf(f'Installer URL: {url}', verbose=True) except OSError as e: - print(f'❌ Error: {e}', file=sys.stderr) + printf(f'❌ {e!s}', fg='bright_red', file=sys.stderr) sys.exit(ExitCode.UNSUPPORTED_PLATFORM) # Download installer to temp location - installer_path = Path.home() / '.hatch_installer.py' - vprint(f'Temporary installer path: {installer_path}') - if not download_installer( + installer_path = Path.home() / '.hatch-universal.pkg' + printf(f'Temporary installer path: {installer_path}', verbose=True) + + # Try the official .pkg installer first; fall back to ``pip install hatch`` if either the download or the installer + # execution fails. Network/SSL errors typically affect the download step. Missing ``sudo`` or platform mismatches + # typically affect the installer step. + installed: bool = False + if download_installer( url, installer_path, + methods=args.download_methods, timeout=args.timeout, retries=args.retries, retry_delay=args.retry_delay, ): - sys.exit(ExitCode.DOWNLOAD_FAILED) - print('') - - # Run the installer - if not run_installer(installer_path): + printf('') + installed = run_installer(installer_path) installer_path.unlink(missing_ok=True) + if not installed: + printf('⚠️ .pkg installer failed; trying pip fallback', fg='bright_yellow') + + if not installed: + installed = install_via_pip() + + if not installed: + printf('', file=sys.stderr) + printf('🔧 Troubleshooting:', fg='bright_white', file=sys.stderr) + printf('1. Check your internet connection', indent=3, file=sys.stderr) + printf('2. Verify you can access https://github.com', indent=3, file=sys.stderr) + printf('3. Check if a proxy is required in your network', indent=3, file=sys.stderr) + printf('4. For SSL errors, try: export SSL_CERT_FILE=$(python3 -m certifi)', indent=3, file=sys.stderr) sys.exit(ExitCode.INSTALL_FAILED) - # Clean up installer - vprint(f'Cleaning up {installer_path}') - installer_path.unlink(missing_ok=True) - # Verify installation if not verify_installation(): sys.exit(ExitCode.VERIFICATION_FAILED) - print('') - print('🎉 Installation complete!\n') - print('📋 Next steps:') - print(' 1. Close and reopen your terminal (or run: source ~/.bashrc)') - print(' 2. Verify with: hatch --version') - print(f' 3. View documentation: {DOCS_URL}') + # We did it! + source_shell_rc_msg: str = '' + if shell := Shell.detect(): + source_shell_rc_msg = f' (or run: {style("source", fg="bright_magenta")} {pathstyle(shell.rc_file)})' + + printf('') + printf('🎉 Installation complete!\n', fg='bright_green', bold=True) + if not args.ensure: + printf('📋 Next steps:', fg='bright_cyan') + printf(f'1. Close and reopen your terminal{source_shell_rc_msg}', indent=3) + printf(f'2. Verify with: {style("hatch --version", fg="bright_magenta")}', indent=3) + printf(f'3. View documentation: {style(DOCS_URL, dim=True)}', indent=3) sys.exit(ExitCode.SUCCESS) diff --git a/scripts/install/install_uv.py b/scripts/install/install_uv.py index b2f326d..96bc998 100755 --- a/scripts/install/install_uv.py +++ b/scripts/install/install_uv.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Programmatically install UV using the official installer script +Programmatically install uv using the official installer script. """ from __future__ import annotations @@ -9,17 +9,28 @@ import functools import os import platform +import re +import shutil +import ssl import subprocess import sys import textwrap import time import urllib.error import urllib.request +from collections.abc import Sequence from pathlib import Path from typing import Any +from typing import Callable from typing import Protocol from typing import TypeVar from typing import Union +from typing import cast + +# ================================================================================================== +# Constants/defaults/globals +# ================================================================================================== + # Configuration MIN_PYTHON: tuple[int, int] = (3, 8) # Minimum supported python version. Script exits with error if not met @@ -35,17 +46,28 @@ VERBOSE: bool = False -PathLike = Union[Path, str] +# ================================================================================================== +# Types/protocols +# ================================================================================================== + + +PathLike = Union[Path, str] +StyleColor = Union[int, tuple[int, int, int], str] + +T_contra = TypeVar('T_contra', contravariant=True) -T_contra = TypeVar('T_contra', contravariant=True) class SupportsWrite(Protocol[T_contra]): - def write(self, s: T_contra, /) -> object: ... + """ + Protocol for writable things (like :attr:`sys.stdout` and :attr:`sys.stderr`). + """ + def write(self, s: T_contra, /) -> object: + ... class ExitCode(int, enum.Enum): """ - Script exit codes + Script exit codes. """ SUCCESS = 0 ALREADY_INSTALLED = 0 @@ -57,135 +79,416 @@ class ExitCode(int, enum.Enum): PYTHON_VERSION = 6 -def vprint(*args: Any, **kwargs: Any) -> None: +class DownloadMethod(str, enum.Enum): """ - Print only if verbose mode is enabled + Installer download methods, ordered by the priority in which they may be attempted. + + ``curl`` is preferred over ``urllib`` by default because curl uses the platform's native trust store + (e.g. SecureTransport on macOS) """ - if VERBOSE: - print(*args, file=kwargs.pop('file', sys.stderr), **kwargs) + CURL = 'curl' + URLLIB = 'urllib' + + def __str__(self) -> str: + return self.value + + +# Default ordered list of download methods to attempt. curl is tried before urllib (see :class:`DownloadMethod`) +DEFAULT_DOWNLOAD_METHODS: tuple[DownloadMethod, ...] = (DownloadMethod.CURL, DownloadMethod.URLLIB) + + +# ================================================================================================== +# Formatting/logging +# ================================================================================================== + +ANSI_COLORS: dict[str, int] = { + 'black': 30, 'bright_black': 90, + 'red': 31, 'bright_red': 91, + 'green': 32, 'bright_green': 92, + 'yellow': 33, 'bright_yellow': 93, + 'blue': 34, 'bright_blue': 94, + 'magenta': 35, 'bright_magenta': 95, + 'cyan': 36, 'bright_cyan': 96, + 'white': 37, 'bright_white': 97, + 'reset': 39, +} +ANSI_RESET_ALL = '\033[0m' -def indent(s: str, num: int) -> str: + +def _interpret_color(_color: StyleColor, offset: int = 0) -> str: """ - Indent string by given number of spaces + Resolve a color name, integer, or RGB tuple to an ANSI SGR parameter string. - :param s: string to indent - :param num: number of spaces to indent each line in string `s` with - :return: indented string + :param _color: color as a name string, 256-color int, or (r, g, b) tuple + :param offset: offset to add for background colors (10 for bg, 0 for fg) + :return: ANSI SGR parameter string (e.g. ``'38;5;196'``) """ - return textwrap.indent(s, ' ' * num) + if isinstance(_color, int): + return f'{38 + offset};5;{_color:d}' + if isinstance(_color, (tuple, list)): + r, g, b = _color + return f'{38 + offset};2;{r:d};{g:d};{b:d}' + _color = cast('str', _color) + return str(ANSI_COLORS[_color] + offset) -def check_python_version() -> bool: +def style( + text: Any, + *, + fg: StyleColor | None = None, + bg: StyleColor | None = None, + bold: bool | None = None, + dim: bool | None = None, + underline: bool | None = None, + overline: bool | None = None, + italic: bool | None = None, + blink: bool | None = None, + reverse: bool | None = None, + strikethrough: bool | None = None, + reset: bool = True, +) -> str: + """ + Style text with ANSI escape codes. + + :param text: the string to style with ansi codes + :param fg: foreground color + :param bg: background color + :param bold: enable or disable bold mode + :param dim: enable or disable dim mode + :param underline: enable or disable underline + :param overline: enable or disable overline + :param italic: enable or disable italic + :param blink: enable or disable blinking + :param reverse: enable or disable inverse rendering + :param strikethrough: enable or disable striking through text + :param reset: add a reset-all code at the end of the string + :return: styled text """ - Check minimum Python version requirement (:attr:`MAX_PYTHON` above) + if not isinstance(text, str): + text = str(text) - :return: True if minimum python version requirement met, False if not + bits: list[str] = [] + if fg: + try: + bits.append(f'\033[{_interpret_color(fg)}m') + except KeyError: + raise TypeError(f'Unknown color {fg!r}') from None + if bg: + try: + bits.append(f'\033[{_interpret_color(bg, 10)}m') + except KeyError: + raise TypeError(f'Unknown color {bg!r}') from None + if bold is not None: + bits.append(f'\033[{1 if bold else 22}m') + if dim is not None: + bits.append(f'\033[{2 if dim else 22}m') + if underline is not None: + bits.append(f'\033[{4 if underline else 24}m') + if overline is not None: + bits.append(f'\033[{53 if overline else 55}m') + if italic is not None: + bits.append(f'\033[{3 if italic else 23}m') + if blink is not None: + bits.append(f'\033[{5 if blink else 25}m') + if reverse is not None: + bits.append(f'\033[{7 if reverse else 27}m') + if strikethrough is not None: + bits.append(f'\033[{9 if strikethrough else 29}m') + + bits.append(text) + if reset: + bits.append(ANSI_RESET_ALL) + return ''.join(bits) + + +def unstyle(text: str) -> str: """ - if sys.version_info < MIN_PYTHON: - print( - f'❌ Error: ' - f'Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+ required, ' - f'but you have Python {sys.version_info.major}.{sys.version_info.minor}', - file=sys.stderr, - ) - return False - else: - vprint(f'✅ Python version {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}') - return True + Remove ANSI styling information from a string. + + :param text: the text to remove style information from + :return: string with ANSI styling characters removed + """ + return re.sub(r'\033\[[;?0-9]*[a-zA-Z]', '', text) -def detect_shell() -> str | None: +def folduser(path: PathLike) -> str: """ - Detect the user's shell + Does the opposite of :meth:`pathlib.Path.expanduser`, replacing the user's home directory with a "~". - :return: current shell + :param path: path + :return: str folded path """ - if shell := os.environ.get('SHELL', ''): - return Path(shell).name - else: - return None + home = str(Path.home()) + return str(path).replace(home, '~') -def get_shell_rc_file() -> Path | None: +def pathstyle(path: PathLike, **kwargs: Any) -> str: """ - Get the appropriate shell RC file path + Style a file path with magenta foreground and home directory condensed to ``~``. - :return: path to shell RC file for current shell + :param path: path to style + :return: styled path string """ - shell = detect_shell() - home = Path.home() + return style(folduser(path), fg='magenta', **kwargs) - shell_rc_map: dict[str, Path] = { - 'bash': home / '.bashrc', - 'zsh': home / '.zshrc', - 'fish': home / '.config' / 'fish' / 'config.fish', - 'tcsh': home / '.tcshrc', - 'csh': home / '.cshrc', - } - # For macOS, bash uses .bash_profile instead of .bashrc - if (shell == 'bash') and (platform.system() == 'Darwin'): - bash_profile = home / '.bash_profile' - if bash_profile.exists(): - return bash_profile +def optstyle(text: Any, **opts: Any) -> str: + """ + Style a CLI option name in yellow. - return shell_rc_map.get(shell) if shell else None + :param text: the option name to style + :param opts: additional keyword arguments forwarded to :func:`style` + :return: ANSI-styled string + """ + return style(text, fg='yellow', **opts) -def get_path_export_command(install_dir: Path) -> str | None: +def typestyle(val: Any, **opts: Any) -> str: """ - Get the command to add directory to PATH based on shell + Style a Python value by its type using semantic colors (roughly matching default repr styling provided by rich). - :param install_dir: directory path - :return: str shell command + Color mapping: + + - ``None`` = magenta italic + - ``bool`` = green (True) / red (False) italic + - ``Path`` = magenta with folduser + - ``int``/``float`` = bright cyan bold + - ``str`` = green + + Other types fall through to ``str(val)`` unstyled + + :param val: the Python value to style + :return: the ANSI-styled representation """ - shell = detect_shell() - if shell in ('bash', 'zsh'): - return f'export PATH="{install_dir}:$PATH"' - elif shell == 'fish': - return f'set -gx PATH "{install_dir}" $PATH' - elif shell in ('tcsh', 'csh'): - return f'setenv PATH "{install_dir}:$PATH"' - else: + if val is None: + return style(str(val), fg='magenta', italic=True, **opts) + if isinstance(val, bool): + return style(str(val), fg=f'bright_{"green" if val else "red"}', italic=True, **opts) + if isinstance(val, Path): + return style(folduser(val), fg='magenta', **opts) + if isinstance(val, (int, float)): + return style(repr(val), fg='bright_cyan', bold=True, **opts) + if isinstance(val, str): + return style(repr(val), fg='green', **opts) + if isinstance(val, Sequence): + return ''.join([ + style('[', fg='bright_white'), + ', '.join(typestyle(item, **opts) for item in val), + style(']', fg='bright_white'), + ]) + return style(val, **opts) + + +def annotated_opt_help( + opt_help: str, + *extra_help_lines: str, + choices: Sequence[Any] | None = None, + default: Any | None = None, + default_fg: StyleColor | None = None, + envvar: str | None = None, + extra_line: bool = True, +) -> str: + """ + Build a styled argparse help string with optional default value and environment variable annotations. + + :param opt_help: core help text for the option (will be styled "bright_white") + :param extra_help_lines: additional help text lines (joined by newlines) for the option. Outputted with no styling + after `opt_help` and before `default`/`envvar` + :param choices: valid option value choices to display below the help text + :param default: default value to display below the help text + :param default_fg: explicit foreground color for the default value (overrides :func:`typestyle`) + :param envvar: environment variable name that can override this option + :param extra_line: if True (default), extra blank line included at the end of the option help text (visually spaces + sequential options apart from each other) + :return: multi-line styled help string with annotations appended + """ + help_lines: list[str] = style(opt_help, fg='bright_white').splitlines() + for extra_help_line in extra_help_lines: + help_lines.extend(extra_help_line.splitlines()) + if choices is not None: + help_lines.append(f' choices: {typestyle(choices)}') + if default is not None: + help_lines.append(f' default: {typestyle(default) if default_fg is None else style(default, fg=default_fg)}') + if envvar is not None: + help_lines.append(f' env var: {style(envvar, fg="cyan")}') + if extra_line: + help_lines.append(' ') + return '\n'.join(help_lines) + + +def printf( + text: str = '', + *, + fg: StyleColor | None = None, + bold: bool | None = None, + dim: bool | None = None, + indent: int = 0, + file: SupportsWrite[str] | None = None, + verbose: bool = False, +) -> None: + """ + Print styled text with optional indent and verbose gating. + + :param text: text to print + :param fg: foreground color + :param bold: bold mode + :param dim: dim mode + :param indent: number of spaces to indent output by + :param file: output file (defaults to stdout, or stderr for verbose output) + :param verbose: only print when global ``VERBOSE`` is True. Output is dim and sent to stderr + """ + if verbose and not VERBOSE: + return + if verbose: + fg = fg or 'bright_white' + dim = True if dim is None else dim + file = file or sys.stderr + + print( + style(textwrap.indent(text, ' ' * indent), fg=fg, bold=bold, dim=dim), + file=file, + ) + + +# ================================================================================================== +# Python checks +# ================================================================================================== + + +def check_python_version() -> bool: + """ + Check minimum Python version requirement (:attr:`MIN_PYTHON` above). + + :return: True if minimum python version requirement met, False if not + """ + if sys.version_info < MIN_PYTHON: + printf( + f'❌ Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+ required, ' + f'but you have Python {sys.version_info.major}.{sys.version_info.minor}', + fg='bright_red', + file=sys.stderr, + ) + return False + vers_str = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}' + printf(f'✅ Python version {vers_str}', verbose=True) + return True + + +# ================================================================================================== +# Shell detection & path mapping +# ================================================================================================== + + +class Shell(enum.Enum): + """ + Known shells with their associated RC file paths and PATH export syntax. + """ + BASH = 'bash' + ZSH = 'zsh' + FISH = 'fish' + TCSH = 'tcsh' + CSH = 'csh' + + @classmethod + def detect(cls) -> Shell | None: + """ + Detect the user's shell from the ``$SHELL`` environment variable. + + :return: detected :class:`Shell` member, or ``None`` if unrecognized or unset + """ + if shell := os.environ.get('SHELL', ''): + name = Path(shell).name + try: + return cls(name) + except ValueError: + return None return None + @property + def rc_file(self) -> Path: + """ + Get the appropriate RC file path for this shell. + + On macOS, bash prefers ``~/.bash_profile`` over ``~/.bashrc`` when the file exists. + + :return: path to the shell's RC file + """ + home = Path.home() + rc_map: dict[Shell, Path] = { + Shell.BASH: home / '.bashrc', + Shell.ZSH: home / '.zshrc', + Shell.FISH: home / '.config' / 'fish' / 'config.fish', + Shell.TCSH: home / '.tcshrc', + Shell.CSH: home / '.cshrc', + } + if (self is Shell.BASH) and (platform.system() == 'Darwin'): + bash_profile = home / '.bash_profile' + if bash_profile.exists(): + return bash_profile + return rc_map[self] + + def path_export_command(self, directory: PathLike) -> str: + """ + Get the command to add a directory to ``PATH`` for this shell. + + :param directory: directory to prepend to ``PATH`` + :return: shell-specific export command string + """ + directory_path = Path(directory).expanduser().resolve() + if self in (Shell.BASH, Shell.ZSH): + return f'export PATH="{directory_path}:$PATH"' + if self is Shell.FISH: + return f'set -gx PATH "{directory_path}" $PATH' + # TCSH, CSH + return f'setenv PATH "{directory_path}:$PATH"' + + +# ================================================================================================== +# `uv` local executable checks +# ================================================================================================== + # TODO: support brew install location as well # TODO: allow installation from current environment as well (from `pip install`) def get_common_install_locations() -> list[Path]: """ - Get common installation locations for UV based on platform + Get common installation locations for uv on macOS. :return: list of filepaths (:class:`pathlib.Path`) """ - home = Path.home() - system = platform.system().lower() - - locations: list[Path] = [] - if system in ('linux', 'darwin'): - locations = [ - home / '.cargo' / 'bin' / 'uv', - home / '.local' / 'bin' / 'uv', - Path('/usr/local/bin/uv'), - Path('/usr/bin/uv'), - ] - elif system == 'windows': - locations = [ - home / '.cargo' / 'bin' / 'uv.exe', - home / '.local' / 'bin' / 'uv.exe', - home / 'AppData' / 'Local' / 'uv' / 'uv.exe', - Path('C:/Program Files/uv/uv.exe'), - ] + home = Path.home() + locations: list[Path] = [ + home / '.cargo' / 'bin' / 'uv', + home / '.local' / 'bin' / 'uv', + Path('/usr/local/bin/uv'), + Path('/usr/bin/uv'), + ] + + # macOS pip ``--user`` installs land in ``~/Library/Python//bin/uv`` (one entry per framework Python). + # Scanning catches uv installed by ``install_via_pip`` even when the system python3 falls back to PEP 370 user-base, + # plus any pre-existing pip --user installs. + library_python = home / 'Library' / 'Python' + if library_python.is_dir(): + for child in sorted(library_python.iterdir()): + uv_path = child / 'bin' / 'uv' + if uv_path not in locations: + locations.append(uv_path) + + # Locations registered dynamically by ``install_via_pip`` (e.g. system python3 scripts dir) + for path in PIP_INSTALL_LOCATIONS: + if path not in locations: + locations.append(path) + return locations def find_executable(*, path_only: bool = False) -> tuple[str | None, str | None]: """ - Try to find the UV executable in common locations + Try to find the uv executable in common locations. :param path_only: if True, only look for executable in PATH. If False, look for executable in other common locations - where UV might be installed + where uv might be installed :return: tuple (executable path, version) """ @@ -223,7 +526,7 @@ def find_executable(*, path_only: bool = False) -> tuple[str | None, str | None] def is_installed(*, quiet: bool = False) -> bool: """ - Check if UV is already installed + Check if uv is already installed. :param quiet: suppress console output if True :return: True if `uv` already installed, False if not @@ -231,32 +534,60 @@ def is_installed(*, quiet: bool = False) -> bool: executable, version = find_executable() if executable: if not quiet: - print(f'✅ UV is already installed: {version}') + printf(f'✅ Already installed: {style(version, fg="bright_white")}', fg='bright_green') if executable != 'uv': - print(f' 📍 Found at: {executable}') - print(' 💡 Note: Consider adding this location to your PATH') + printf(f'Found at: {pathstyle(executable)}', indent=3) + printf( + style('NOTE: ', fg='magenta', bold=True) + + style('consider adding this to your PATH', fg='bright_white'), + indent=3, + ) return True - else: - return False + return False + + +# ================================================================================================== +# `uv` installer retrieval/execution +# ================================================================================================== def get_installer_url() -> str: """ - Get the appropriate UV installer URL based on the platform + Get the uv installer URL for macOS. - :raises OSError: if current platform/operating system is not supported + :raises OSError: if current platform is not macOS :return: installer URL """ - system = platform.system().lower() - if system in ('linux', 'darwin'): - return 'https://astral.sh/uv/install.sh' - elif system == 'windows': - return 'https://astral.sh/uv/install.ps1' + if platform.system().lower() != 'darwin': + raise OSError(f'This installer only supports macOS (got: {platform.system()})') + return 'https://astral.sh/uv/install.sh' + + +def write_installer(dest: PathLike, content: bytes) -> bool: + """ + Write downloaded installer bytes to disk, surfacing helpful troubleshooting tips on failure. + + :param dest: destination file path + :param content: bytes to write + :return: True if successful, False if not + """ + try: + with Path(dest).expanduser().open('wb') as f: + f.write(content) + except (PermissionError, OSError) as e: + printf(f'❌ Failed to write installer to disk: {e!s}', fg='bright_red', file=sys.stderr) + printf('', file=sys.stderr) + printf('🔧 Troubleshooting:', fg='bright_white', file=sys.stderr) + printf(f'1. Check write permissions for: {pathstyle(dest)}', indent=3, file=sys.stderr) + printf('2. Verify sufficient disk space is available', indent=3, file=sys.stderr) + printf('3. Ensure the parent directory exists', indent=3, file=sys.stderr) + return False else: - raise OSError(f'Unsupported operating system: {system}') + printf(f'✅ Downloaded to {pathstyle(dest)}', fg='bright_green') + return True -def download_installer( +def download_with_urllib( url: str, dest: PathLike, *, @@ -265,60 +596,177 @@ def download_installer( retry_delay: float = DOWNLOAD_RETRY_DELAY, ) -> bool: """ - Download the installer script with retry logic + Download a file using Python's :mod:`urllib`. - :param url: installer URL - :param dest: where to download installer to - :param timeout: download timeout in seconds - :param retries: maximum number of download retry attempts + Aborts early on SSL verification errors so the curl fallback can be tried sooner. + SSL errors are not transient and additional retries will not recover. + + :param url: source URL + :param dest: destination file path + :param timeout: per-request timeout in seconds + :param retries: maximum number of retry attempts :param retry_delay: initial delay in seconds between retries, before exponential backoff :return: True if successful, False if not """ + content: bytes | None = None for attempt in range(1, retries + 1): try: if attempt > 1: delay = retry_delay * (2 ** (attempt - 2)) # Exponential backoff - print(f'⏱️ Retrying in {delay} seconds... (attempt {attempt}/{retries})') + printf(f'⏱️ Retrying in {delay} seconds... (attempt {attempt}/{retries})', fg='bright_yellow') time.sleep(delay) - print(f'⏬ Downloading installer from {url}...') - vprint(f' Attempt {attempt}/{retries}') + printf(f'⏬ Downloading installer from {url} (urllib)...', bold=True) + printf(f'Attempt {attempt}/{retries}', indent=3, verbose=True) - with urllib.request.urlopen(url, timeout=timeout) as response: + # A real-browser User-Agent is required: the default ``Python-urllib/X.Y`` is rejected (403) by the + # bot-protection in front of ``releases.astral.sh`` (where ``astral.sh/uv/install.sh`` redirects to) + # rdar://177045555 ([ezcli] `install_uv.py` fails to download installer with "HTTP Error 403: Forbidden") + request = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) # noqa: S310 + with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 content = response.read() - with Path(dest).expanduser().open('wb') as f: - f.write(content) except urllib.error.URLError as e: - print(f'❌ Download failed: {e}', file=sys.stderr) + printf(f'❌ urllib download failed: {e!s}', fg='bright_red', file=sys.stderr) + if isinstance(e.reason, ssl.SSLError): + printf('SSL verification failed; aborting urllib retries', indent=3, verbose=True, file=sys.stderr) + return False if attempt == retries: - print('', file=sys.stderr) - print('🔧 Troubleshooting:', file=sys.stderr) - print(' 1. Check your internet connection', file=sys.stderr) - print(' 2. Verify you can access https://astral.sh', file=sys.stderr) - print(' 3. Check if a proxy is required in your network', file=sys.stderr) return False except Exception as e: - print(f'❌ Unexpected error during download: {e}', file=sys.stderr) + printf(f'❌ Unexpected error during urllib download: {e!s}', fg='bright_red', file=sys.stderr) if attempt == retries: return False + # Download succeeded, break out of retry loop else: - print(f'✅ Downloaded to {dest}') + break + + if content is None: + return False + return write_installer(dest, content) + + +def download_with_curl( + url: str, + dest: PathLike, + *, + timeout: float = DOWNLOAD_TIMEOUT, + retries: int = DOWNLOAD_RETRIES, + retry_delay: float = DOWNLOAD_RETRY_DELAY, +) -> bool: + """ + Download a file using the system ``curl`` executable. + + Useful as a fallback when :mod:`urllib` fails with SSL certificate verification errors — since curl uses the + platform's native trust store (e.g. SecureTransport on macOS) + + :param url: source URL + :param dest: destination file path + :param timeout: per-request timeout in seconds + :param retries: maximum number of retry attempts + :param retry_delay: initial delay in seconds between retries, before exponential backoff + :return: True if successful, False if not + """ + curl = shutil.which('curl') + if curl is None: + printf('curl not available, skipping curl fallback', verbose=True, file=sys.stderr) + return False + + dest_path = Path(dest).expanduser() + curl_cmd: list[str] = [ + curl, + '--fail', + '--silent', + '--show-error', + '--location', + '--max-time', str(int(timeout)), + '--output', str(dest_path), + url, + ] + + for attempt in range(1, retries + 1): + if attempt > 1: + delay = retry_delay * (2 ** (attempt - 2)) # Exponential backoff + printf(f'⏱️ Retrying in {delay} seconds... (attempt {attempt}/{retries})', fg='bright_yellow') + time.sleep(delay) + + printf(f'⏬ Downloading installer from {url} (curl)...', bold=True) + printf(f'Attempt {attempt}/{retries}', indent=3, verbose=True) + printf(f'Running: {" ".join(curl_cmd)}', indent=3, verbose=True) + try: + subprocess.run(curl_cmd, check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + err = (e.stderr or '').strip() or str(e) + printf(f'❌ curl download failed: {err}', fg='bright_red', file=sys.stderr) + if attempt == retries: + return False + else: + printf(f'✅ Downloaded to {pathstyle(dest_path)}', fg='bright_green') + return True + + return False + + +# Dispatch table mapping each download method to its implementation. +# Both functions share the same signature ``(url, dest, *, timeout, retries, retry_delay) -> bool`` +DOWNLOAD_FUNCS: dict[DownloadMethod, Callable[..., bool]] = { + DownloadMethod.CURL: download_with_curl, + DownloadMethod.URLLIB: download_with_urllib, +} + + +def download_installer( + url: str, + dest: PathLike, + *, + methods: Sequence[DownloadMethod] = DEFAULT_DOWNLOAD_METHODS, + timeout: float = DOWNLOAD_TIMEOUT, + retries: int = DOWNLOAD_RETRIES, + retry_delay: float = DOWNLOAD_RETRY_DELAY, +) -> bool: + """ + Download the installer script, trying each configured download method in order until one succeeds. + + The default order (see :attr:`DEFAULT_DOWNLOAD_METHODS`) tries ``curl`` before :mod:`urllib`. curl handles + environments where Python's bundled OpenSSL CA list cannot validate the TLS chain to ``url`` + + :param url: installer URL + :param dest: where to download installer to + :param methods: ordered download methods to attempt; each is tried until one succeeds + :param timeout: download timeout in seconds + :param retries: maximum number of download retry attempts + :param retry_delay: initial delay in seconds between retries, before exponential backoff + :raises ValueError: if `url` does not start with "http:" or "https:" + :return: True if successful, False if not + """ + if not url.startswith(('http:', 'https:')): + raise ValueError("URL must start with 'http:' or 'https:'") + + ordered_methods = list(methods) or list(DEFAULT_DOWNLOAD_METHODS) + for index, method in enumerate(ordered_methods): + download_func = DOWNLOAD_FUNCS[method] + if download_func(url, dest, timeout=timeout, retries=retries, retry_delay=retry_delay): return True + # Announce the fallback to the next method, if any remain + if index < len(ordered_methods) - 1: + next_method = ordered_methods[index + 1] + printf(f'⚠️ {method} download failed; falling back to {next_method}', fg='bright_yellow') + printf('') + return False -def run_installer_unix(installer_path: PathLike) -> bool: +def run_installer(installer_path: PathLike) -> bool: """ - Execute the UV installer script on Unix-like systems + Execute the uv installer shell script. :param installer_path: path to installer script :return: True on success, False on failure """ - print('💿 Running UV installer...') + printf('💿 Running uv installer...', bold=True) try: result = subprocess.run( ['sh', str(installer_path)], @@ -327,245 +775,371 @@ def run_installer_unix(installer_path: PathLike) -> bool: text=True, ) except subprocess.CalledProcessError as e: - print(f'❌ Installation failed: {e}', file=sys.stderr) - print(indent(e.stderr, 3), file=sys.stderr) + printf(f'❌ Installation failed: {e!s}', fg='bright_red', file=sys.stderr) + printf(e.stderr, indent=3, file=sys.stderr) return False else: - print(indent(result.stdout, 3)) + printf(result.stdout, indent=3) if result.stderr: - print(indent(result.stderr, 3)) + printf(result.stderr, indent=3) return True -def run_installer_windows(installer_path: PathLike) -> bool: +# ================================================================================================== +# `pip`-based installation (last-resort fallback) +# ================================================================================================== + + +# Locations registered dynamically by :func:`install_via_pip` so :func:`find_executable` can find uv after a +# successful pip install on this run. +PIP_INSTALL_LOCATIONS: list[Path] = [] + + +def resolve_system_python() -> str | None: """ - Execute the UV installer script on Windows + Locate a system ``python3`` executable suitable for the pip-install fallback. - :param installer_path: path to installer script - :return: True on success, False on failure + Prefers the macOS system interpreter at ``/usr/bin/python3``, then anything named ``python3`` on ``PATH``. + + :return: absolute path to a python3 executable, or ``None`` if none can be found + """ + system_python = Path('/usr/bin/python3') + if system_python.exists(): + return str(system_python) + return shutil.which('python3') + + +def register_pip_install_location(python_exe: str) -> None: + """ + Register the scripts directory of ``python_exe`` so :func:`find_executable` can find ``uv``. + + Asks the interpreter for its ``sysconfig`` scripts path and appends ``/uv`` to the module-level + :attr:`PIP_INSTALL_LOCATIONS` list. + + :param python_exe: path to a python interpreter that just ran ``pip install uv`` """ - print('💿 Running UV installer...') try: result = subprocess.run( - ['powershell', '-ExecutionPolicy', 'Bypass', '-File', str(installer_path)], + [python_exe, '-c', 'import sysconfig; print(sysconfig.get_path("scripts"))'], check=True, capture_output=True, text=True, ) - except subprocess.CalledProcessError as e: - print(f'❌ Installation failed: {e}', file=sys.stderr) - print(indent(e.stderr, 3), file=sys.stderr) - return False + except (subprocess.CalledProcessError, FileNotFoundError): + return else: - print(indent(result.stdout, 3)) - if result.stderr: - print(indent(result.stderr, 3)) - return True + candidate = Path(result.stdout.strip()) / 'uv' + if candidate not in PIP_INSTALL_LOCATIONS: + PIP_INSTALL_LOCATIONS.append(candidate) + printf(f'Registered pip install location: {pathstyle(candidate)}', indent=3, verbose=True) -def run_installer(installer_path: PathLike) -> bool: +def install_via_pip() -> bool: """ - Execute the UV installer script based on platform + Install uv via ``python3 -m pip install uv`` using the system ``python3`` executable. + + Used as a last-resort fallback when neither :mod:`urllib` nor ``curl`` could fetch the official installer script. + Installs into the system Python's site-packages so the resulting ``uv`` executable lands in that interpreter's + scripts directory. - :param installer_path: path to installer script :return: True on success, False on failure """ - system = platform.system().lower() - if system in ('linux', 'darwin'): - return run_installer_unix(installer_path) - elif system == 'windows': - return run_installer_windows(installer_path) - else: - print(f'❌ Unsupported platform: {system}', file=sys.stderr) + printf('') + printf('💿 Falling back to pip install uv (system python3)...', bold=True) + + python3 = resolve_system_python() + if python3 is None: + printf('❌ Could not find a python3 executable for pip fallback', fg='bright_red', file=sys.stderr) + return False + printf(f'Using python: {pathstyle(python3)}', indent=3, verbose=True) + + cmd: list[str] = [python3, '-m', 'pip', 'install', 'uv'] + printf(f'Running: {" ".join(cmd)}', indent=3, verbose=True) + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + + except subprocess.CalledProcessError as e: + printf(f'❌ pip install uv failed (exit {e.returncode})', fg='bright_red', file=sys.stderr) + if e.stdout: + printf(e.stdout.rstrip(), indent=3, file=sys.stderr) + if e.stderr: + printf(e.stderr.rstrip(), indent=3, file=sys.stderr) + return False + + except FileNotFoundError as e: + printf(f'❌ pip install uv failed: {e!s}', fg='bright_red', file=sys.stderr) return False + else: + if VERBOSE and result.stdout: + printf(result.stdout.rstrip(), indent=3, verbose=True) + printf('✅ Installed uv via pip', fg='bright_green') + register_pip_install_location(python3) + return True + def verify_installation() -> bool: """ - Verify that UV was installed successfully. Provides detailed guidance if tool is installed but not in PATH + Verify that uv was installed successfully. Provides detailed guidance if tool is installed but not in PATH. :return: True on success, False on failure """ - print('') - print('🔍 Verifying installation...') - vprint('Checking all known installation locations...') + printf('') + printf('🔍 Verifying installation...', bold=True) + printf('Checking all known installation locations...', indent=3, verbose=True) # Use find_executable to check all locations executable, version = find_executable() if not executable: - print('❌ UV installation could not be verified\n', file=sys.stderr) - print('🔧 Troubleshooting:', file=sys.stderr) - print(' 1. Close and reopen your terminal', file=sys.stderr) - print(' 2. Try running the installer again with --force', file=sys.stderr) - print(' 3. Check the official UV installation guide:', file=sys.stderr) - print(f' {INSTALLATION_GUIDE_URL}', file=sys.stderr) + printf('❌ uv installation could not be verified\n', fg='bright_red', file=sys.stderr) + printf('🔧 Troubleshooting:', fg='bright_white', file=sys.stderr) + printf('1. Close and reopen your terminal', indent=3, file=sys.stderr) + printf(f'2. Try running the installer again with {optstyle("--force")}', indent=3, file=sys.stderr) + printf('3. Check the official uv installation guide:', indent=3, file=sys.stderr) + printf(INSTALLATION_GUIDE_URL, dim=True, indent=6, file=sys.stderr) return False # Tool found! - print(f'✅ UV installed successfully: {version}') + printf('✅ Installed successfully', fg='bright_green') + printf(version or '', indent=3) # Check if it's in PATH if executable == 'uv': - print('✅ UV is in your PATH and ready to use') - vprint(f' Executable: {executable}') + printf('✅ In your PATH and ready to use', fg='bright_green') + printf(f'Executable: {executable}', indent=3, verbose=True) return True # Tool is installed but not in PATH + printf(f'📍 Found at: {pathstyle(executable)}\n') + printf('⚠️ Installed but not in your PATH', fg='bright_yellow') + + install_dir = Path(executable).parent + shell = Shell.detect() + + printf('') + printf('💡 To make it available globally, add it to your PATH:\n', fg='bright_white') + if shell: + rc_file = shell.rc_file + export_cmd = shell.path_export_command(install_dir) + printf(f'For {shell.value}, add this line to {pathstyle(rc_file)}:', indent=3) + printf(style(export_cmd, fg='bright_magenta'), indent=3) + printf('') + printf('Then run:', indent=3) + printf(style(f'source {folduser(rc_file)}', fg='bright_magenta'), indent=3) else: - print(f'📍 Found at: {executable}\n') - print('⚠️ UV is installed but not in your PATH') - - install_dir = Path(executable).parent - shell = detect_shell() - rc_file = get_shell_rc_file() - export_cmd = get_path_export_command(install_dir) - - print('') - print('💡 To make UV available globally, add it to your PATH:\n') - if rc_file and export_cmd and shell: - print(f' For {shell}, add this line to {rc_file}:') - print(f' {export_cmd}\n') - print(' Then run:') - print(f' source {rc_file}') - else: - print(f' Add {install_dir} to your PATH environment variable') - print(" Consult your shell's documentation for instructions") + printf(f'Add {pathstyle(install_dir)} to your PATH environment variable', indent=3) + printf("Consult your shell's documentation for instructions", indent=3, dim=True) - print('') - print(f' Or use the full path: {executable}') + printf('') + printf(f'Or use the full path: {pathstyle(executable)}', indent=3) + + # Still consider this a success since the tool is installed + return True - # Still consider this a success since the tool is installed - return True +# ================================================================================================== +# Command-line arguments +# ================================================================================================== -def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + +class Args(argparse.Namespace): + """ + Annotated :class:`argparse.Namespace` returned by :func:`parse_args`. + """ + force: bool # -f/--force + ensure: bool # -e/--ensure + verbose: bool # -v/--verbose + timeout: float # -t/--timeout + retries: int # -r/--retries + retry_delay: float # --retry-delay + download_methods: list[DownloadMethod] # -m/--download-method + + +def parse_args(argv: Sequence[str] | None = None) -> Args: """ - Build parser and parse command-line arguments + Build parser and parse command-line arguments. - :param argv: argument list of parse, defaults to ``sys.argv[1:]`` - :return: :class:`argparse.Namespace` with parsed argument values + :param argv: argument list of parse. Defaults to ``sys.argv[1:]`` + :return: annotated namespace of parsed args (:class:`Args`) """ global VERBOSE + PROG = style('%(prog)s', fg='bright_magenta') + + def dim(text: Any, **opts: Any) -> str: + return style(text, dim=opts.pop('dim', True), **opts) + # Build argument parser parser = argparse.ArgumentParser( - description='Install UV package manager using official installer script', + description=style('Install uv package manager using official installer script', fg='bright_yellow'), formatter_class=functools.partial( - argparse.RawDescriptionHelpFormatter, - max_help_position=50, + argparse.RawTextHelpFormatter, + max_help_position=80, ), epilog='\n'.join([ - 'Examples:', + style('Examples:', fg='bright_blue', bold=True), '', - ' %(prog)s # Normal installation', - ' %(prog)s --force # Force reinstall even if present', - ' %(prog)s --verbose # Show detailed progress', - ' %(prog)s --force -v # Force reinstall with verbose output', - ' %(prog)s --retries 5 # Increase download retry attempts to 5', - ' %(prog)s --retry-delay 3 # Set initial retry delay to 3 seconds', - ' %(prog)s --retries 5 --retry-delay 3 # Custom retry configuration', + f' {PROG} {dim("# Normal installation")}', + f' {PROG} --force {dim("# Force reinstall even if present")}', + f' {PROG} --verbose {dim("# Show detailed progress")}', + f' {PROG} --force -v {dim("# Force reinstall with verbose output")}', + f' {PROG} --retries 5 {dim("# Increase download retry attempts to 5")}', + f' {PROG} --retry-delay 3 {dim("# Set initial retry delay to 3 seconds")}', + f' {PROG} --retries 5 --retry-delay 3 {dim("# Custom retry configuration")}', + f' {PROG} --download-method curl {dim("# Only use curl to download the installer")}', + f' {PROG} --download-method urllib curl {dim("# Try urllib first, then fall back to curl")}', ]), add_help=False, ) - execution_opts = parser.add_argument_group('Execution options') - execution_opts.add_argument( - '-f', - '--force', - action='store_true', - help='Force installation even if UV is already installed', - ) - execution_opts.add_argument( - '-e', - '--ensure', - action='store_true', - help='Install UV if it does not exist, otherwise quietly exit with no console output', - ) - - download_opts = parser.add_argument_group('Download options') - download_opts.add_argument( - '-t', - '--timeout', - type=float, - default=DOWNLOAD_TIMEOUT, - metavar='SECONDS', - help=f'Download timeout in seconds (default: {DOWNLOAD_TIMEOUT})', - ) - download_opts.add_argument( - '-r', - '--retries', - type=int, - default=DOWNLOAD_RETRIES, - metavar='N', - help=f'Maximum number of download retry attempts (default: {DOWNLOAD_RETRIES})', - ) - download_opts.add_argument( - '--retry-delay', - type=float, - default=DOWNLOAD_RETRY_DELAY, - metavar='SECONDS', - help=f'Initial delay in seconds between retries, uses exponential backoff (default: {DOWNLOAD_RETRY_DELAY})', - ) - - logging_opts = parser.add_argument_group('Logging options') - logging_opts.add_argument( - '-v', - '--verbose', - action='store_true', - help='Enable verbose output for debugging', - ) - - other_opts = parser.add_argument_group('Other options') - other_opts.add_argument( - '-h', - '--help', - action='help', - default=argparse.SUPPRESS, - help='Show this help message and exit', - ) + def add_execution_opts() -> None: + """ + Add execution option arguments (``--force``, ``--ensure``) to the parser. + """ + execution_opts = parser.add_argument_group(style('Execution options', fg='bright_blue', bold=True)) + execution_opts.add_argument( + '-f', + '--force', + action='store_true', + help=annotated_opt_help('Force installation even if uv is already installed'), + ) + execution_opts.add_argument( + '-e', + '--ensure', + action='store_true', + help=annotated_opt_help('Install uv if it does not exist, otherwise quietly exit with no console output'), + ) + add_execution_opts() + + def add_download_opts() -> None: + """ + Add download option arguments (``--timeout``, ``--retries``, ``--retry-delay``) to the parser. + """ + download_opts = parser.add_argument_group(style('Download options', fg='bright_blue', bold=True)) + download_opts.add_argument( + '-t', + '--timeout', + type=float, + default=DOWNLOAD_TIMEOUT, + metavar='SECONDS', + help=annotated_opt_help( + 'Download timeout in seconds', + default=DOWNLOAD_TIMEOUT, + ), + ) + download_opts.add_argument( + '-r', + '--retries', + type=int, + default=DOWNLOAD_RETRIES, + metavar='NUM', + help=annotated_opt_help( + 'Maximum number of download retry attempts', + default=DOWNLOAD_RETRIES, + ) + ) + download_opts.add_argument( + '-d', + '--retry-delay', + type=float, + default=DOWNLOAD_RETRY_DELAY, + metavar='SECONDS', + help=annotated_opt_help( + 'Initial delay in seconds between retries, uses exponential backoff', + default=DOWNLOAD_RETRY_DELAY, + ), + ) + download_opts.add_argument( + '-m', + '--download-method', + dest='download_methods', + nargs='+', + type=DownloadMethod, + choices=list(DownloadMethod), + default=list(DEFAULT_DOWNLOAD_METHODS), + metavar='METHOD', + help=annotated_opt_help( + 'Ordered download methods to try, highest priority first', + choices=[m.value for m in DownloadMethod], + default=[m.value for m in DEFAULT_DOWNLOAD_METHODS], + ), + ) + add_download_opts() + + def add_other_opts() -> None: + """ + Add miscellaneous option arguments (``--help``) to the parser. + """ + other_opts = parser.add_argument_group(style('Other options', fg='bright_blue', bold=True)) + other_opts.add_argument( + '-v', + '--verbose', + action='store_true', + help='Enable verbose output for debugging', + ) + other_opts.add_argument( + '-h', + '--help', + action='help', + default=argparse.SUPPRESS, + help='Show this help message and exit', + ) + add_other_opts() # Parse arguments - args = parser.parse_args(argv) + args = parser.parse_args(argv, namespace=Args()) # Set global config variables with parsed command-line option values VERBOSE = args.verbose # Validate download parameters if args.timeout < 0: - print('❌ Error: --timeout cannot be negative', file=sys.stderr) + printf(f'❌ {optstyle("--timeout")} cannot be negative', fg='bright_red', file=sys.stderr) sys.exit(ExitCode.INVALID_PARAMETERS) if args.retries < 1: - print('❌ Error: --retries must be at least 1', file=sys.stderr) + printf(f'❌ {optstyle("--retries")} must be at least 1', fg='bright_red', file=sys.stderr) sys.exit(ExitCode.INVALID_PARAMETERS) if args.retry_delay < 0: - print('❌ Error: --retry-delay cannot be negative', file=sys.stderr) + printf(f'❌ {optstyle("--retry-delay")} cannot be negative', fg='bright_red', file=sys.stderr) sys.exit(ExitCode.INVALID_PARAMETERS) # Enforce mutual exclusivity of --force and --ensure # Using ``add_mutually_exclusive_group()`` prevents us from adding a help text title to the execution options group if args.force and args.ensure: - print('❌ Error: --force and --ensure are mutually exclusive', file=sys.stderr) + printf( + f'❌ {optstyle("--force")} and {optstyle("--ensure")} are mutually exclusive', + fg='bright_red', + file=sys.stderr, + ) sys.exit(ExitCode.INVALID_PARAMETERS) return args -def main(argv: list[str] | None = None) -> None: +# ================================================================================================== +# Core script entry point +# ================================================================================================== + + +def main(argv: Sequence[str] | None = None) -> None: """ - Programmatically install UV using the official installer script + Programmatically install uv using the official installer script. - :param argv: argument list of parse, defaults to ``sys.argv[1:]`` + :param argv: argument list of parse. Defaults to ``sys.argv[1:]`` """ # Parse command-line arguments args = parse_args(argv=argv) if args.verbose or not args.ensure: - print('╔════════════════════════════╗') - print('║ UV Installation Script ║') - print('╚════════════════════════════╝') - print() - vprint(f'Download retry configuration: {args.retries} attempts, {args.retry_delay}s initial delay\n') + printf() + printf('uv installation', fg='bright_cyan', bold=True) + printf('─' * 77, dim=True) + printf( + f'Download configuration: methods={" → ".join(str(m) for m in args.download_methods)}, ' + f'{args.retries} attempts, {args.retry_delay}s initial delay\n', + verbose=True, + ) # Check Python version if not check_python_version(): @@ -575,56 +1149,74 @@ def main(argv: list[str] | None = None) -> None: if not args.force: if is_installed(quiet=args.ensure and not args.verbose): if not args.ensure: - print('⏭️ Skipping installation\n') - print('💡 Tip: Use --force to reinstall') + printf('⏭️ Skipping installation') + printf( + f'{style("Use", fg="bright_white")} {optstyle("--force")} ' + f'{style("to reinstall", fg="bright_white")}', + indent=3, + ) sys.exit(ExitCode.ALREADY_INSTALLED) else: - print('🔄 --force specified, proceeding with installation...\n') + printf(f'🔄 {optstyle("--force")} specified, proceeding with installation...\n', fg='bright_cyan') # Get installer URL try: url = get_installer_url() - vprint(f'Installer URL: {url}') + printf(f'Installer URL: {url}', verbose=True) except OSError as e: - print(f'❌ Error: {e}', file=sys.stderr) + printf(f'❌ {e!s}', fg='bright_red', file=sys.stderr) sys.exit(ExitCode.UNSUPPORTED_PLATFORM) - # Determine installer file extension - system = platform.system().lower() - extension = '.ps1' if system == 'windows' else '.sh' - installer_path = Path.home() / f'.uv_installer{extension}' - vprint(f'Temporary installer path: {installer_path}') + # Download installer to temp location + installer_path = Path.home() / '.uv_installer.sh' + printf(f'Temporary installer path: {installer_path}', verbose=True) - # Download installer - if not download_installer( + # Try the official installer first; fall back to ``pip install uv`` if either the download or the installer + # execution fails. Network/SSL errors typically affect the download step; broken shells or platform mismatches + # typically affect the installer step + installed: bool = False + if download_installer( url, installer_path, + methods=args.download_methods, timeout=args.timeout, retries=args.retries, retry_delay=args.retry_delay, ): - sys.exit(ExitCode.DOWNLOAD_FAILED) - - # Run the installer - print() - if not run_installer(installer_path): + printf('') + installed = run_installer(installer_path) installer_path.unlink(missing_ok=True) + if not installed: + printf('⚠️ Installer script failed; trying pip fallback', fg='bright_yellow') + + if not installed: + installed = install_via_pip() + + if not installed: + printf('', file=sys.stderr) + printf('🔧 Troubleshooting:', fg='bright_white', file=sys.stderr) + printf('1. Check your internet connection', indent=3, file=sys.stderr) + printf('2. Verify you can access https://astral.sh', indent=3, file=sys.stderr) + printf('3. Check if a proxy is required in your network', indent=3, file=sys.stderr) + printf('4. For SSL errors, try: export SSL_CERT_FILE=$(python3 -m certifi)', indent=3, file=sys.stderr) sys.exit(ExitCode.INSTALL_FAILED) - # Clean up installer - vprint(f'Cleaning up {installer_path}') - installer_path.unlink(missing_ok=True) - # Verify installation if not verify_installation(): sys.exit(ExitCode.VERIFICATION_FAILED) - print('') - print('🎉 Installation complete!\n') - print('📋 Next steps:') - print(' 1. Close and reopen your terminal (or run: source ~/.bashrc)') - print(' 2. Verify with: uv --version') - print(f' 3. View documentation: {DOCS_URL}') + # We did it! + source_shell_rc_msg: str = '' + if shell := Shell.detect(): + source_shell_rc_msg = f' (or run: {style("source", fg="bright_magenta")} {pathstyle(shell.rc_file)})' + + printf('') + printf('🎉 Installation complete!\n', fg='bright_green', bold=True) + if not args.ensure: + printf('📋 Next steps:', fg='bright_cyan') + printf(f'1. Close and reopen your terminal{source_shell_rc_msg}', indent=3) + printf(f'2. Verify with: {style("uv --version", fg="bright_magenta")}', indent=3) + printf(f'3. View documentation: {style(DOCS_URL, dim=True)}', indent=3) sys.exit(ExitCode.SUCCESS) diff --git a/scripts/maintain/README.md b/scripts/maintain/README.md new file mode 100644 index 0000000..445034e --- /dev/null +++ b/scripts/maintain/README.md @@ -0,0 +1,139 @@ +[Project Root](../../README.md) > [Scripts](../README.md) > **Maintain Scripts** + +--- + +# Maintain Scripts + +Python scripts for automating common codebase maintenance tasks: keeping `.gitignore` patterns current and prek hook +versions up to date. + +## Scripts + +| Script | Language | Description | +| --------------------- | -------- | --------------------------------------------------------------------------------------------- | +| `update_gitignore.py` | Python | Refresh `.gitignore` with latest patterns from gitignore.io while preserving custom additions | +| `update_prek.py` | Python | Bump prek hook versions via `prek auto-update` | + +--- + +## update_gitignore.py + +Fetches fresh ignore patterns for the template list embedded in the current `.gitignore`, splices them in, and preserves +any custom patterns that appear after the generated block. Uses three fetch methods in order: Python `urllib`, `curl`, +and `git-ignore-io`. + +**Usage:** + +```bash +# Update .gitignore with latest patterns +uv run scripts/maintain/update_gitignore.py + +# Preview the updated content without writing the file +uv run scripts/maintain/update_gitignore.py --dry-run + +# Force rewrite even when no content has changed +uv run scripts/maintain/update_gitignore.py --force + +# Target a different .gitignore file +uv run scripts/maintain/update_gitignore.py --gitignore-path path/to/.gitignore +``` + +**Options:** + +| Flag | Default | Description | +| ----------------------- | ------------ | ------------------------------------------------ | +| `--dry-run` | off | Print the updated content; do not write the file | +| `--force` | off | Write even when content is unchanged | +| `--gitignore-path PATH` | `.gitignore` | Path to the `.gitignore` file to update | + +**Behavior:** + +1. Reads the current `.gitignore` and extracts the template list from the embedded gitignore.io URL. +2. Extracts any custom patterns that appear after the `# End of https://www.toptal.com/developers/gitignore/api/` + marker. +3. Fetches fresh content from `toptal.com/developers/gitignore/api/{templates}` (falls back to `curl`, then + `git-ignore-io`). +4. Combines the fresh content with the preserved custom patterns. +5. Skips the write if content is unchanged (unless `--force`). + +**Public API (`__all__`):** + +| Symbol | Signature | Description | +| ---------------------------------- | --------------------------------------------------------- | ------------------------------------------------------ | +| `extract_templates_from_gitignore` | `(gitignore_content: str) -> list[str]` | Parse template names from the gitignore.io URL | +| `extract_custom_patterns` | `(gitignore_content: str) -> list[str]` | Extract lines below the generated-content end marker | +| `fetch_gitignore_content` | `(templates: list[str]) -> str` | Fetch fresh content from gitignore.io (with fallbacks) | +| `generate_updated_gitignore` | `(fresh_content: str, custom_patterns: list[str]) -> str` | Combine fresh content with preserved custom patterns | + +**Dependencies:** Python standard library only (`argparse`, `re`, `urllib`, `subprocess`). + +--- + +## update_prek.py + +Thin wrapper around `prek auto-update` that adds dry-run support, targeted repo filtering, tag/cooldown constraints, and +a configurable timeout. In dry-run mode the config file is reverted to its original state after auto-update runs, so the +diff is visible without any permanent changes. + +**Usage:** + +```bash +# Update all hooks in .pre-commit-config.yaml +uv run scripts/maintain/update_prek.py + +# Preview what would change without persisting the update +uv run scripts/maintain/update_prek.py --dry-run + +# Update only a specific repo +uv run scripts/maintain/update_prek.py --repo https://github.com/astral-sh/ruff-pre-commit + +# Update multiple repos +uv run scripts/maintain/update_prek.py \ + --repo https://github.com/astral-sh/ruff-pre-commit \ + --repo https://github.com/pre-commit/pre-commit-hooks + +# Only bump versions released at least 7 days ago, using a longer timeout +uv run scripts/maintain/update_prek.py --cooldown-days 7 --timeout 240 +``` + +**Options:** + +| Flag | Default | Description | +| ------------------------- | ------------------------- | ------------------------------------------------------------- | +| `-c, --config PATH` | `.pre-commit-config.yaml` | Path to the prek/pre-commit config file | +| `-r, --repo URL` | (all repos) | Restrict update to this repo URL; repeat for multiple repos | +| `--exclude-repo URL` | (none) | Skip the given repo URL; repeat for multiple repos | +| `--include-tag PATTERN` | (all tags) | Only consider tags matching this glob pattern; repeatable | +| `--exclude-tag PATTERN` | (none) | Ignore tags matching this glob pattern; repeatable | +| `--repo-include-tag SPEC` | (none) | Per-repo tag include filter as `=`; repeatable | +| `--repo-exclude-tag SPEC` | (none) | Per-repo tag exclude filter as `=`; repeatable | +| `--bleeding-edge` | off | Update to the default branch head instead of the latest tag | +| `--freeze` | off | Store frozen hashes in `rev` instead of tag names | +| `--cooldown-days DAYS` | (none) | Minimum release age (in days) for a version to be eligible | +| `-j, --jobs N` | (prek default) | Number of threads prek should use (`0` lets prek decide) | +| `--refresh` | off | Refresh all cached prek data before running | +| `-t, --timeout SECONDS` | `120` | Timeout for the `prek auto-update` subprocess | +| `--dry-run` | off | Run auto-update, show changes, then revert the file | +| `-h, --help` | — | Show help and exit | + +**Behavior:** + +1. Reads the current config file content for later comparison / revert. +2. Runs `prek auto-update` (optionally scoped to specific repos and tag/cooldown constraints). +3. Prints stdout from auto-update (`Updating ... -> ` lines). +4. If `--dry-run` and changes were made, reverts the file to its original state. +5. Reports whether any hook versions changed. + +**Public API (`__all__`):** + +| Symbol | Signature | Description | +| ---------------- | -------------------------------------------------------------------- | --------------------------------------------- | +| `run_autoupdate` | `(config_path: Path, *, repo_urls=None, ..., timeout=120, **kwargs)` | Run `prek auto-update` and return the process | + +**Dependencies:** Python standard library only (`argparse`, `subprocess`, `pathlib`). Requires `prek` on PATH. + +--- + +## See Also + +- [Scripts Overview](../README.md) — Parent directory documentation diff --git a/scripts/maintain/update_gitignore.py b/scripts/maintain/update_gitignore.py new file mode 100755 index 0000000..2ff41df --- /dev/null +++ b/scripts/maintain/update_gitignore.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +""" +Script to update .gitignore file with latest patterns from gitignore.io while preserving custom patterns. + +Usage: + + python scripts/maintain/update_gitignore.py [--dry-run] [--force] + +""" +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +from functools import partial +from pathlib import Path +from typing import TYPE_CHECKING +from urllib.error import URLError +from urllib.request import urlopen + +if TYPE_CHECKING: + from collections.abc import Sequence + +__all__ = [ + 'extract_custom_patterns', + 'extract_templates_from_gitignore', + 'fetch_gitignore_content', + 'generate_updated_gitignore', +] + + +def extract_templates_from_gitignore(gitignore_content: str) -> list[str]: + """ + Extract template list from gitignore.io URL in the current .gitignore file. + + :param gitignore_content: Content of the current .gitignore file + :return: list of template names used + """ + if m := re.search(r'https://www\.toptal\.com/developers/gitignore/api/([a-zA-Z0-9+,]+)', gitignore_content): + templates_string = m.group(1) + return templates_string.split(',') + raise ValueError('Could not find gitignore.io URL in current .gitignore file') + + +def extract_custom_patterns(gitignore_content: str) -> list[str]: + """ + Extract custom patterns added after the gitignore.io generated content. + + :param gitignore_content: context of the current .gitignore file + :return: list of custom pattern lines + """ + lines = gitignore_content.splitlines() + + # Find the end marker for gitignore.io content + end_marker_idx: int | None = None + for i, line in enumerate(lines): + if re.match(r'# End of https://www\.toptal\.com/developers/gitignore/api/', line): + end_marker_idx = i + break + + if end_marker_idx is None: + print('Warning: Could not find end marker for gitignore.io content', file=sys.stderr) + return [] + + # Extract everything after the end marker, ignoring empty lines + custom_lines: list[str] = [] + for line in lines[end_marker_idx + 1:]: + if (not custom_lines) and (not line.strip()): # Skip empty lines at the beginning + continue + custom_lines.append(line) + + # Remove trailing empty lines + while custom_lines and not custom_lines[-1].strip(): + custom_lines.pop() + return custom_lines + + +def fetch_gitignore_content(templates: list[str]) -> str: + """ + Fetch fresh .gitignore content from gitignore.io for the given templates. Tries multiple methods as fallbacks. + + :param templates: list of template names + :return: fresh gitignore content from gitignore.io + """ + templates_str = ','.join(templates) + url = f'https://www.toptal.com/developers/gitignore/api/{templates_str}' + + # Method 1: Try Python's urllib first + try: + with urlopen(url) as response: # noqa: S310 + content = response.read().decode('utf-8') + print('✅ Successfully fetched content using Python urllib') + return content + + except URLError as e: + print(f'⚠️ urllib failed ({e}), trying curl...') + + # Method 2: Try curl as fallback + try: + result = subprocess.run( + ['curl', '-sSL', url], + check=False, + capture_output=True, + text=True, + timeout=30 + ) + if (result.returncode == 0) and result.stdout: + print('✅ Successfully fetched content using curl') + return result.stdout + print(f'⚠️ curl failed (exit code {result.returncode}), trying git-ignore-io...') + + except (subprocess.TimeoutExpired, FileNotFoundError) as e: + print(f'⚠️ curl failed ({e}), trying git-ignore-io...') + + # Method 3: Try git-ignore-io as final fallback + try: + result = subprocess.run( + ['git-ignore-io', templates_str], + check=False, + capture_output=True, + text=True, + timeout=30, + ) + if result.returncode == 0 and result.stdout: + print('✅ Successfully fetched content using git-ignore-io') + return result.stdout + raise RuntimeError(f'git-ignore-io failed with exit code {result.returncode}') + + except (subprocess.TimeoutExpired, FileNotFoundError) as e: + raise RuntimeError(f'All methods failed. Final attempt (git-ignore-io): {e}') from e + + +def generate_updated_gitignore(fresh_content: str, custom_patterns: list[str]) -> str: + """ + Combine fresh gitignore.io content with custom patterns. + + :param fresh_content: fresh content from gitignore.io + :param custom_patterns: custom patterns to preserve + :return: combined gitignore content + """ + + # Ensure fresh content ends with exactly one newline + fresh_content = fresh_content.rstrip('\n') + '\n' + if not custom_patterns: + return fresh_content + + # Add custom patterns after a blank line + result = fresh_content + '\n' + for pattern in custom_patterns: + result += pattern + '\n' + return result + + +class Args(argparse.Namespace): + """ + Annotated :class:`argparse.Namespace` returned by :func:`parse_args`. + """ + gitignore_path: Path # --gitignore-path + dry_run: bool # --dry-run + force: bool # --force + + +def build_parser() -> argparse.ArgumentParser: + """ + Build argument parser with auto-generated help from command docstrings. + + :return: :class:`argparse.ArgumentParser` + """ + parser = argparse.ArgumentParser( + description='Update .gitignore file with latest patterns from gitignore.io', + formatter_class=partial(argparse.HelpFormatter, max_help_position=80), + add_help=False, + ) + + def add_file_opts() -> None: + """ + Add file path options to the argument parser. + """ + file_opts = parser.add_argument_group('File options') + file_opts.add_argument( + '--gitignore-path', + type=Path, + default=Path('.gitignore'), + help='Path to .gitignore file (default: .gitignore)', + ) + add_file_opts() + + def add_execution_opts() -> None: + """ + Add execution control options to the argument parser. + """ + execution_opts = parser.add_argument_group('Execution opts') + execution_opts.add_argument( + '--dry-run', + action='store_true', + help='Show what would be changed without actually updating the file', + ) + execution_opts.add_argument( + '--force', + action='store_true', + help='Update the file even if no changes are detected', + ) + add_execution_opts() + + def add_other_opts() -> None: + """ + Add help and miscellaneous options to the argument parser. + """ + other_opts = parser.add_argument_group('Other options') + other_opts.add_argument( + '-h', + '--help', + action='help', + default=argparse.SUPPRESS, + help='Show help message and exit', + ) + add_other_opts() + + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + """ + Main entry point for the script. + + :param argv: argument list of parse. Defaults to ``sys.argv[1:]`` + :return: int script exit code + """ + parser = build_parser() + args = parser.parse_args(argv, namespace=Args()) + + # Resolve gitignore path + if not args.gitignore_path.exists(): + print(f'Error: .gitignore file not found at {args.gitignore_path}', file=sys.stderr) + return 1 + + try: + + # Read current .gitignore + current_content = args.gitignore_path.read_text(encoding='utf-8') + + # Extract templates and custom patterns + print('Extracting templates and custom patterns...') + templates = extract_templates_from_gitignore(current_content) + custom_patterns = extract_custom_patterns(current_content) + + print(f"Found templates: {', '.join(templates)}") + if custom_patterns: + print(f'Found {len(custom_patterns)} custom pattern(s)') + else: + print('No custom patterns found') + + # Fetch fresh content from gitignore.io + print('Fetching fresh content from gitignore.io...') + fresh_content = fetch_gitignore_content(templates) + + # Generate updated content + updated_content = generate_updated_gitignore(fresh_content, custom_patterns) + + # Check if content has actually changed + if (not args.force) and (updated_content == current_content): + print('✅ .gitignore file is already up to date') + return 0 + if args.dry_run: + print('🔍 Dry run - would update .gitignore file with the following content:') + print('=' * 50) + print(updated_content) + print('=' * 50) + return 0 + + # Write updated content + args.gitignore_path.write_text(updated_content, encoding='utf-8') + print(f'✅ Successfully updated {args.gitignore_path}') + return 0 + + except KeyboardInterrupt: + print('Cancelled') + return 1 + + except Exception as e: + print(f'Error: {e}', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/maintain/update_prek.py b/scripts/maintain/update_prek.py new file mode 100755 index 0000000..8ceedc9 --- /dev/null +++ b/scripts/maintain/update_prek.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 +""" +Script to update pre-commit hook versions using ``prek auto-update``. + +Usage:: + + python scripts/maintain/update_prek.py [--dry-run] [--repo URL] [--config PATH] + +""" +from __future__ import annotations + +import argparse +import subprocess +import sys +from functools import partial +from pathlib import Path +from typing import TYPE_CHECKING +from typing import Any + +if TYPE_CHECKING: + from collections.abc import Sequence + +__all__ = [ + 'run_autoupdate', +] + + +def run_autoupdate( + config_path: Path, + *, + repo_urls: list[str] | None = None, + exclude_repos: list[str] | None = None, + include_tags: list[str] | None = None, + exclude_tags: list[str] | None = None, + repo_include_tags: list[str] | None = None, + repo_exclude_tags: list[str] | None = None, + bleeding_edge: bool = False, + freeze: bool = False, + jobs: int | None = None, + cooldown_days: int | None = None, + refresh: bool = False, + timeout: float | None = 120, + **kwargs: Any, +) -> subprocess.CompletedProcess[str]: + """ + Run ``prek auto-update`` and return the completed process. + + :param config_path: path to the pre-commit config file + :param repo_urls: optional list of repo URLs to filter updates + :param exclude_repos: optional list of repo URLs to exclude from updates + :param include_tags: optional list of glob patterns for tags to consider + :param exclude_tags: optional list of glob patterns for tags to ignore + :param repo_include_tags: optional list of ``=`` per-repo tag include filters + :param repo_exclude_tags: optional list of ``=`` per-repo tag exclude filters + :param bleeding_edge: update to the bleeding edge of the default branch instead of latest tag + :param freeze: store frozen hashes in ``rev`` instead of tag names + :param jobs: number of threads prek should use (``0`` lets prek decide) + :param cooldown_days: minimum release age (in days) for a version to be eligible + :param refresh: refresh all cached prek data before running + :param timeout: command timeout + :return: completed process from the auto-update command + """ + cmd: list[str] = ['prek', 'auto-update', '--config', str(config_path)] + if refresh: + cmd.append('--refresh') + if bleeding_edge: + cmd.append('--bleeding-edge') + if freeze: + cmd.append('--freeze') + if jobs is not None: + cmd.extend(['--jobs', str(jobs)]) + if cooldown_days is not None: + cmd.extend(['--cooldown-days', str(cooldown_days)]) + for url in repo_urls or []: + cmd.extend(['--repo', url]) + for url in exclude_repos or []: + cmd.extend(['--exclude-repo', url]) + for pattern in include_tags or []: + cmd.extend(['--include-tag', pattern]) + for pattern in exclude_tags or []: + cmd.extend(['--exclude-tag', pattern]) + for spec in repo_include_tags or []: + cmd.extend(['--repo-include-tag', spec]) + for spec in repo_exclude_tags or []: + cmd.extend(['--repo-exclude-tag', spec]) + + return subprocess.run( + cmd, + capture_output=kwargs.pop('capture_output', True), + text=kwargs.pop('text', True), + check=kwargs.pop('check', False), + timeout=timeout, + **kwargs, + ) + + +class Args(argparse.Namespace): + """ + Annotated :class:`argparse.Namespace` for script command-line arguments. + """ + config: Path # --config + repos: list[str] | None # --repo + exclude_repos: list[str] | None # --exclude-repo + include_tags: list[str] | None # --include-tag + exclude_tags: list[str] | None # --exclude-tag + repo_include_tags: list[str] | None # --repo-include-tag + repo_exclude_tags: list[str] | None # --repo-exclude-tag + bleeding_edge: bool # --bleeding-edge + freeze: bool # --freeze + cooldown_days: int | None # --cooldown-days + jobs: int | None # --jobs + refresh: bool # --refresh + timeout: float # --timeout + dry_run: bool # --dry-run + + +def build_parser() -> argparse.ArgumentParser: + """ + Build argument parser for script. + + :return: :class:`argparse.ArgumentParser` + """ + parser = argparse.ArgumentParser( + description='Update pre-commit hook versions using prek auto-update', + formatter_class=partial(argparse.HelpFormatter, max_help_position=50), + add_help=False, + ) + + def add_target_opts() -> None: + """ + Add config file and repo target options to the argument parser. + """ + target_opts = parser.add_argument_group('Target options') + target_opts.add_argument( + '-c', + '--config', + type=Path, + metavar='PATH', + default='.pre-commit-config.yaml', + help='Path to pre-commit config file (default: .pre-commit-config.yaml)', + ) + target_opts.add_argument( + '-r', + '--repo', + action='append', + dest='repos', + metavar='URL', + help='Only update the given repo (can be repeated)', + ) + target_opts.add_argument( + '--exclude-repo', + action='append', + dest='exclude_repos', + metavar='URL', + help='Skip the given repo (can be repeated)', + ) + target_opts.add_argument( + '--include-tag', + action='append', + dest='include_tags', + metavar='PATTERN', + help='Only consider tags matching this glob pattern (can be repeated)', + ) + target_opts.add_argument( + '--exclude-tag', + action='append', + dest='exclude_tags', + metavar='PATTERN', + help='Ignore tags matching this glob pattern (can be repeated)', + ) + target_opts.add_argument( + '--repo-include-tag', + action='append', + dest='repo_include_tags', + metavar='REPO=PATTERN', + help='Per-repo tag include filter as = (can be repeated)', + ) + target_opts.add_argument( + '--repo-exclude-tag', + action='append', + dest='repo_exclude_tags', + metavar='REPO=PATTERN', + help='Per-repo tag exclude filter as = (can be repeated)', + ) + add_target_opts() + + def add_update_opts() -> None: + """ + Add update behavior options to the argument parser. + """ + update_opts = parser.add_argument_group('Update options') + update_opts.add_argument( + '--bleeding-edge', + action='store_true', + help='Update to the bleeding edge of the default branch instead of latest tag', + ) + update_opts.add_argument( + '--freeze', + action='store_true', + help='Store frozen hashes in `rev` instead of tag names', + ) + update_opts.add_argument( + '--cooldown-days', + type=int, + metavar='DAYS', + default=None, + help='Minimum release age (in days) required for a version to be eligible', + ) + add_update_opts() + + def add_execution_opts() -> None: + """ + Add execution control options to the argument parser. + """ + execution_opts = parser.add_argument_group('Execution options') + execution_opts.add_argument( + '-j', + '--jobs', + type=int, + metavar='N', + default=None, + help='Number of threads prek should use (0 lets prek decide)', + ) + execution_opts.add_argument( + '--refresh', + action='store_true', + help='Refresh all cached prek data before running', + ) + execution_opts.add_argument( + '-t', + '--timeout', + metavar='SECONDS', + type=float, + default=120, + help='`prek auto-update` command timeout (in seconds)', + ) + execution_opts.add_argument( + '--dry-run', + action='store_true', + help='Run auto-update, show what changed, but revert the file', + ) + add_execution_opts() + + def add_other_opts() -> None: + """ + Add help and miscellaneous options to the argument parser. + """ + other_opts = parser.add_argument_group('Other options') + other_opts.add_argument( + '-h', + '--help', + action='help', + default=argparse.SUPPRESS, + help='Show help message and exit', + ) + add_other_opts() + + return parser + + +# TODO: ensure prek is installed (or install it if not?) +def main(argv: Sequence[str] | None = None) -> int: + """ + Main entry point for the script. + + :param argv: argument list to parse, defaults to sys.argv[1:] + :return: int script exit code + """ + parser = build_parser() + args = parser.parse_args(argv, namespace=Args()) + + if args.timeout < 0: + print('Error: --timeout cannot be negative', file=sys.stderr) + return 1 + if (args.cooldown_days is not None) and (args.cooldown_days < 0): + print('Error: --cooldown-days cannot be negative', file=sys.stderr) + return 1 + if (args.jobs is not None) and (args.jobs < 0): + print('Error: --jobs cannot be negative', file=sys.stderr) + return 1 + + config_path = Path(args.config) + if not config_path.exists(): + print(f'Error: config file not found at {config_path}', file=sys.stderr) + return 1 + + try: + + # Read current config content + original_content = config_path.read_text(encoding='utf-8') + + # Run prek auto-update + print('Running prek auto-update...') + result = run_autoupdate( + config_path, + repo_urls=args.repos, + exclude_repos=args.exclude_repos, + include_tags=args.include_tags, + exclude_tags=args.exclude_tags, + repo_include_tags=args.repo_include_tags, + repo_exclude_tags=args.repo_exclude_tags, + bleeding_edge=args.bleeding_edge, + freeze=args.freeze, + jobs=args.jobs, + cooldown_days=args.cooldown_days, + refresh=args.refresh, + timeout=args.timeout, + ) + + # Show stdout from auto-update (contains "Updating ... -> " lines) + if result.stdout: + print(result.stdout.rstrip()) + if result.stderr: + print(result.stderr.rstrip(), file=sys.stderr) + + if result.returncode != 0: + print(f'Error: prek auto-update exited with code {result.returncode}', file=sys.stderr) + return 1 + + # Read the (possibly updated) config + updated_content = config_path.read_text(encoding='utf-8') + + # Detect changes + if has_changes := original_content != updated_content: + print('Changes detected in pre-commit config') + else: + print('No changes detected - hooks are already up to date') + + if args.dry_run: + + # Restore original content if updated but --dry-run requested + if has_changes: + config_path.write_text(original_content, encoding='utf-8') + print('Dry run - reverted config file to original content') + + return 0 + + if has_changes: + print(f'Successfully updated {config_path}') + return 0 + + except subprocess.TimeoutExpired: + print('Error: prek auto-update timed out', file=sys.stderr) + return 1 + + except KeyboardInterrupt: + print('Cancelled') + return 1 + + except Exception as e: + print(f'Error: {e}', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/utility/generate_completions.py b/scripts/utility/generate_completions.py new file mode 100755 index 0000000..69d79c5 --- /dev/null +++ b/scripts/utility/generate_completions.py @@ -0,0 +1,782 @@ +#!/usr/bin/env python3 +""" +Generate shell completion scripts for argparse-based CLI tools. + +Supports bash, zsh, and fish. Can auto-detect an :class:`argparse.ArgumentParser` from a target script by looking for +well-known factory functions (``build_parser``, ``get_parser``, etc.) or module-level parser instances. + +Usage:: + + # Auto-detect parser factory in a script + python scripts/utility/generate_completions.py scripts/maintain/update_prek.py zsh + + # Explicit parser factory function name + python scripts/utility/generate_completions.py scripts/maintain/update_prek.py zsh --parser build_parser + + # Override the command name used in the completion script + python scripts/utility/generate_completions.py scripts/maintain/update_prek.py zsh --name update_prek + +""" +from __future__ import annotations + +import argparse +import importlib.util +import re +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING +from typing import ClassVar +from typing import Literal +from typing import get_args + +if TYPE_CHECKING: + from collections.abc import Callable + from collections.abc import Sequence + from types import ModuleType + + +SupportedShell = Literal['bash', 'zsh', 'fish'] + +# Repo-relative path to this generator, used in the header comments of generated completion scripts. +GENERATOR_PATH: str = 'scripts/utility/generate_completions.py' + +PARSER_FACTORY_NAMES: list[str] = [ + 'build_parser', + 'get_parser', + 'create_parser', + 'make_parser', + 'parser', +] + +# ========================= +# String manipulation utils +# ========================= + + +ANSI_PATTERN: re.Pattern[str] = re.compile(r'\033\[[;?0-9]*[a-zA-Z]') + + +def strip_ansi(text: str) -> str: + """ + Remove ANSI escape sequences from a string. + + :param text: text potentially containing ANSI codes + :return: cleaned text + """ + return ANSI_PATTERN.sub('', text) + + +# ========================== +# Parser metadata extraction +# ========================== + + +@dataclass(frozen=True) +class CompletionOption: + """ + Extracted argument metadata for shell completion generation. + """ + flags: tuple[str, ...] + help: str + takes_value: bool + metavar: str | None = None + choices: tuple[str, ...] | None = None + + +def extract_parser_options(parser: argparse.ArgumentParser) -> list[CompletionOption]: + """ + Extract option metadata from an :class:`argparse.ArgumentParser` for completion generation. + + Skips subparser actions and positional arguments. Splits :class:`argparse.BooleanOptionalAction` into separate + entries for ``--flag`` and ``--no-flag``. + + :param parser: the parser to introspect + :return: list of extracted option info + """ + options: list[CompletionOption] = [] + for action in parser._actions: + if isinstance(action, argparse._SubParsersAction): + continue + if not action.option_strings: + continue + + help_text = '' + if action.help and (action.help != argparse.SUPPRESS): + help_text = strip_ansi(str(action.help)) + help_text = help_text.split('[')[0].strip() + help_text = help_text.splitlines()[0].strip() if help_text else '' + + takes_value = not isinstance( + action, + ( + argparse._StoreTrueAction, + argparse._StoreFalseAction, + argparse._StoreConstAction, + argparse._CountAction, + argparse._HelpAction, + argparse.BooleanOptionalAction, + ), + ) + + choices = tuple(str(c) for c in action.choices) if action.choices else None + metavar = action.metavar if isinstance(action.metavar, str) else None + + if isinstance(action, argparse.BooleanOptionalAction): + options.extend( + CompletionOption( + flags=(opt_str,), + help=help_text if not opt_str.startswith('--no-') else '', + takes_value=False, + ) + for opt_str in action.option_strings + ) + + else: + options.append( + CompletionOption( + flags=tuple(action.option_strings), + help=help_text, + takes_value=takes_value, + metavar=metavar, + choices=choices, + ) + ) + + return options + + +def extract_subparser_choices(parser: argparse.ArgumentParser) -> dict[str, argparse.ArgumentParser]: + """ + Extract the subcommand-name-to-subparser mapping from a parser. + + :param parser: the root parser + :return: mapping of command name to its subparser + """ + for action in parser._actions: + if isinstance(action, argparse._SubParsersAction): + return dict(action.choices) + return {} + + +def extract_positional_choices(parser: argparse.ArgumentParser) -> list[tuple[str, tuple[str, ...]]]: + """ + Extract positional arguments with constrained choices from a parser. + + :param parser: the parser to introspect + :return: list of ``(help_text, choices)`` tuples for positional arguments that define choices + """ + positionals: list[tuple[str, tuple[str, ...]]] = [] + for action in parser._actions: + if action.option_strings or isinstance(action, (argparse._SubParsersAction, argparse._HelpAction)): + continue + if action.choices: + help_text = '' + if action.help and (action.help != argparse.SUPPRESS): + help_text = strip_ansi(str(action.help)).split('[')[0].strip() + positionals.append((help_text, tuple(str(c) for c in action.choices))) + return positionals + + +def get_subparser_description(subparser: argparse.ArgumentParser) -> str: + """ + Get the first line of a subparser's description, with ANSI codes stripped. + + :param subparser: the subparser + :return: first line of description, or empty string + """ + desc = subparser.description or '' + desc = strip_ansi(desc) + return desc.splitlines()[0].strip() if desc else '' + + +# =========== +# zsh helpers +# =========== + + +def zsh_escape(text: str) -> str: + """ + Escape text for zsh completion spec strings. + + :param text: raw description text + :return: text safe for use inside single-quoted zsh specs + """ + return text.replace("'", "'\\''").replace('[', '\\[').replace(']', '\\]').replace(':', '\\:') + + +def zsh_option_specs(opt: CompletionOption) -> list[str]: + """ + Format a :class:`CompletionOption` as one or more zsh ``_arguments`` spec strings. + + Generates a separate spec for each flag string (short and long variants). + + :param opt: the option to format + :return: list of single-quoted zsh _arguments specs + """ + desc = zsh_escape(opt.help) if opt.help else '' + + specs: list[str] = [] + for flag in opt.flags: + if opt.takes_value: + eq = '=' if flag.startswith('--') else '' + argname = opt.metavar or 'value' + choices_str = f'({" ".join(opt.choices)})' if opt.choices else '' + specs.append(f"'{flag}{eq}[{desc}]:{argname}:{choices_str}'") + else: + specs.append(f"'{flag}[{desc}]'") + return specs + + +# ===================== +# Completion generators +# ===================== + + +@dataclass +class CompletionConfig: + """ + Configuration for completion script generation. + + :param command_name: command name for the completion function (e.g. ``update_prek.py``) + :param script_path: display path to the script (for header comments) + :param project_name: project name (for header comments) + """ + command_name: str + script_path: str = '' + project_name: str = '' + + FUNC_NAME_CHARS: ClassVar[re.Pattern[str]] = re.compile(r'[^a-zA-Z0-9_]') + + @property + def func_name(self) -> str: + """ + Shell-safe function name derived from the command name. + + :return: function name prefixed with ``_`` + """ + return '_' + self.FUNC_NAME_CHARS.sub('_', self.command_name) + + +def generate_zsh_completions(parser: argparse.ArgumentParser, config: CompletionConfig) -> str: + """ + Generate a zsh completion script from an argument parser. + + :param parser: the fully built root argument parser (with subparsers) + :param config: completion configuration + :return: the complete zsh completion script + """ + cmd = config.command_name + func = config.func_name + + lines: list[str] = [ + f'#compdef {cmd}', + '', + f'# Shell completions for {cmd}' + (f' ({config.project_name})' if config.project_name else ''), + ] + if config.script_path: + lines.extend([ + f'# Generated by: python {GENERATOR_PATH} {config.script_path} zsh', + '#', + '# Installation:', + f'# eval "$(python {GENERATOR_PATH} {config.script_path} zsh)"', + '#', + '# Or save to a file in your fpath:', + f'# python {GENERATOR_PATH} {config.script_path} zsh > ~/.zsh/completions/_{cmd}', + '#', + f"# For aliases (e.g. alias myalias='python {config.script_path}'):", + f'# compdef {func} myalias', + ]) + lines.extend(['', f'{func}() {{', ' local state', '']) + + # Build _arguments spec with root options + subcommand dispatch + root_options = extract_parser_options(parser) + + specs: list[str] = [spec for opt in root_options for spec in zsh_option_specs(opt)] + specs.append("'1:command:->commands'") + specs.append("'*::arg:->args'") + + lines.append(' _arguments -C \\') + for i, spec in enumerate(specs): + sep = ' \\' if i < len(specs) - 1 else '' + lines.append(f' {spec}{sep}') + + # Command completion + subparser_choices = extract_subparser_choices(parser) + lines.extend([ + '', + ' case $state in', + ' commands)', + ' local -a commands=(', + ]) + for cmd_name, subparser in subparser_choices.items(): + desc = zsh_escape(get_subparser_description(subparser)) + lines.append(f" '{cmd_name}:{desc}'") + + lines.extend([ + ' )', + " _describe 'command' commands", + ' ;;', + ]) + + # Per-command argument completion + lines.extend([' args)', ' case $words[1] in']) + for cmd_name, subparser in subparser_choices.items(): + cmd_options = extract_parser_options(subparser) + cmd_positionals = extract_positional_choices(subparser) + if (not cmd_options) and (not cmd_positionals): + continue + + cmd_specs = [spec for opt in cmd_options for spec in zsh_option_specs(opt)] + for pos_help, pos_choices in cmd_positionals: + desc = zsh_escape(pos_help) if pos_help else '' + cmd_specs.append(f"':{desc}:({' '.join(pos_choices)})'") + + lines.extend([ + f' {cmd_name})', + ' _arguments \\' + ]) + for i, spec in enumerate(cmd_specs): + sep = ' \\' if i < len(cmd_specs) - 1 else '' + lines.append(f' {spec}{sep}') + lines.append(' ;;') + + lines.extend([ + ' esac', + ' ;;', + ' esac', + '}', + '', + f'{func} "$@"', + '', + ]) + return '\n'.join(lines) + + +def generate_bash_completions(parser: argparse.ArgumentParser, config: CompletionConfig) -> str: + """ + Generate a bash completion script from an argument parser. + + :param parser: the fully built root argument parser (with subparsers) + :param config: completion configuration + :return: the complete bash completion script + """ + cmd = config.command_name + func = config.func_name + subparser_choices = extract_subparser_choices(parser) + + # Collect all command names + cmd_names = list(subparser_choices.keys()) + + # Collect global option strings + root_options = extract_parser_options(parser) + global_opts = ' '.join(flag for opt in root_options for flag in opt.flags) + + lines: list[str] = [ + '#!/usr/bin/env bash', + '', + f'# Shell completions for {cmd}' + (f' ({config.project_name})' if config.project_name else ''), + ] + if config.script_path: + lines.extend([ + f'# Generated by: python {GENERATOR_PATH} {config.script_path} bash', + '#', + '# Installation:', + f'# eval "$(python {GENERATOR_PATH} {config.script_path} bash)"', + '#', + '# Or save to a file:', + f'# python {GENERATOR_PATH} {config.script_path} bash > ~/.bash_completions/{cmd}', + '#', + f"# For aliases (e.g. alias myalias='python {config.script_path}'):", + f'# complete -F {func} myalias', + ]) + lines.extend([ + '', + f'{func}() {{', + ' local cur cmd', + ' COMPREPLY=()', + ' cur="${COMP_WORDS[COMP_CWORD]}"', + '', + ' # Find the subcommand', + ' cmd=""', + ' local i', + ' for ((i=1; i str: + """ + Generate a fish completion script from an argument parser. + + :param parser: the fully built root argument parser (with subparsers) + :param config: completion configuration + :return: the complete fish completion script + """ + cmd = config.command_name + subparser_choices = extract_subparser_choices(parser) + + lines: list[str] = [ + f'# Shell completions for {cmd}' + (f' ({config.project_name})' if config.project_name else ''), + ] + if config.script_path: + lines.extend([ + f'# Generated by: python {GENERATOR_PATH} {config.script_path} fish', + '#', + '# Installation:', + f'# python {GENERATOR_PATH} {config.script_path} fish > ~/.config/fish/completions/{cmd}.fish', + '#', + '# For aliases, add:', + f"# alias myalias='python {config.script_path}'", + f'# complete -c myalias --wraps {cmd}', + ]) + lines.extend([ + '', + '# Disable file completions by default', + f'complete -c {cmd} -f', + '', + '# Commands', + ]) + + for cmd_name, subparser in subparser_choices.items(): + desc = get_subparser_description(subparser).replace("'", "\\'") + lines.append(f"complete -c {cmd} -n '__fish_use_subcommand' -a '{cmd_name}' -d '{desc}'") + + # Global options + if root_options := extract_parser_options(parser): + lines.extend([ + '', + '# Global options', + ]) + for opt in root_options: + desc = opt.help.replace("'", "\\'") if opt.help else '' + for flag in opt.flags: + if flag.startswith('--'): + long_name = flag[2:] + req_arg = ' -r' if opt.takes_value else '' + lines.append(f"complete -c {cmd} -l '{long_name}'{req_arg} -d '{desc}'") + elif flag.startswith('-') and len(flag) == 2: + short_name = flag[1] + req_arg = ' -r' if opt.takes_value else '' + lines.append(f"complete -c {cmd} -s '{short_name}'{req_arg} -d '{desc}'") + + # Per-command options + for cmd_name, subparser in subparser_choices.items(): + cmd_options = extract_parser_options(subparser) + cmd_positionals = extract_positional_choices(subparser) + if (not cmd_options) and (not cmd_positionals): + continue + + lines.extend(['', f'# Options for: {cmd_name}']) + for opt in cmd_options: + desc = opt.help.replace("'", "\\'") if opt.help else '' + for flag in opt.flags: + if flag.startswith('--'): + long_name = flag[2:] + req_arg = ' -r' if opt.takes_value else '' + lines.append( + f"complete -c {cmd} -n '__fish_seen_subcommand_from {cmd_name}'" + f" -l '{long_name}'{req_arg} -d '{desc}'" + ) + elif flag.startswith('-') and (len(flag) == 2): + short_name = flag[1] + req_arg = ' -r' if opt.takes_value else '' + lines.append( + f"complete -c {cmd} -n '__fish_seen_subcommand_from {cmd_name}'" + f" -s '{short_name}'{req_arg} -d '{desc}'" + ) + for pos_help, pos_choices in cmd_positionals: + desc = pos_help.replace("'", "\\'") if pos_help else '' + lines.append( + f"complete -c {cmd} -n '__fish_seen_subcommand_from {cmd_name}'" + f" -a '{' '.join(pos_choices)}' -d '{desc}'" + ) + + lines.append('') + return '\n'.join(lines) + + +COMPLETION_GENERATORS: dict[SupportedShell, Callable[[argparse.ArgumentParser, CompletionConfig], str]] = { + 'bash': generate_bash_completions, + 'zsh': generate_zsh_completions, + 'fish': generate_fish_completions, +} + + +def generate_completions( + shell: SupportedShell, + parser: argparse.ArgumentParser, + *, + command_name: str | None = None, + script_path: str | None = None, + project_name: str | None = None, +) -> str: + """ + Generate a shell completion script for the given shell. + + :param shell: target shell (bash, zsh, or fish) + :param parser: the fully built root argument parser + :param command_name: command name for the completion function (defaults to "cli") + :param script_path: display path to the script (for header comments) + :param project_name: project name (for header comments) + :raises ValueError: if *shell* is not a supported shell name + :return: the complete shell completion script + """ + generator = COMPLETION_GENERATORS.get(shell) + if generator is None: + raise ValueError( + f'Unsupported shell: {shell!r}. ' + f'Supported shells: {", ".join(sorted(get_args(SupportedShell)))}' + ) + + return generator( + parser, + CompletionConfig( + command_name=command_name or 'cli', + script_path=script_path or '', + project_name=project_name or '', + ), + ) + + +# ============== +# Auto-detection +# ============== + + +def import_module_from_path(script_path: Path) -> ModuleType: + """ + Dynamically import a Python script as a module. + + :param script_path: path to the script file + :raises FileNotFoundError: if the script does not exist + :raises ImportError: if the script cannot be loaded + :return: the imported module + """ + if not script_path.is_file(): + raise FileNotFoundError(f'Script not found: {script_path}') + + spec = importlib.util.spec_from_file_location(script_path.stem, script_path) + if (spec is None) or (spec.loader is None): + raise ImportError(f'Cannot load module spec from: {script_path}') + + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def detect_parser(module: ModuleType) -> argparse.ArgumentParser: + """ + Auto-detect an :class:`argparse.ArgumentParser` from a loaded module. + + Tries calling well-known factory functions first (``build_parser``, ``get_parser``, ``create_parser``, + ``make_parser``, ``parser``), then falls back to searching for module-level ``ArgumentParser`` instances. + + :param module: the imported module to inspect + :raises RuntimeError: if no parser can be detected + :return: the detected argument parser + """ + + # Try well-known factory function names + for name in PARSER_FACTORY_NAMES: + attr = getattr(module, name, None) + if not callable(attr): + continue + + try: + result = attr() + except Exception: # noqa: S112 — intentional: probing unknown callables during auto-detection + continue + if isinstance(result, argparse.ArgumentParser): + return result + + # Fall back to module-level ArgumentParser instances + for name in dir(module): + if name.startswith('_'): + continue + attr = getattr(module, name, None) + if isinstance(attr, argparse.ArgumentParser): + return attr + + raise RuntimeError( + f'Could not auto-detect an ArgumentParser in {module.__name__!r}. ' + f'Looked for factory functions ({", ".join(PARSER_FACTORY_NAMES)}) and module-level ArgumentParser instances. ' + f'Use --parser to specify the factory function name explicitly.' + ) + + +def resolve_parser(script_path: Path, *, parser_name: str | None = None) -> argparse.ArgumentParser: + """ + Import a script and resolve its argument parser. + + When *parser_name* is provided, it is looked up as an attribute on the imported module and called (if callable). + Otherwise, :func:`detect_parser` is used for auto-detection. + + :param script_path: path to the target script + :param parser_name: optional name of a callable or attribute that provides the parser + :raises RuntimeError: if the parser cannot be resolved + :return: the resolved argument parser + """ + module = import_module_from_path(script_path) + + if parser_name is not None: + attr = getattr(module, parser_name, None) + if attr is None: + raise RuntimeError(f'Attribute {parser_name!r} not found in {script_path}') + + if callable(attr): + result = attr() + if not isinstance(result, argparse.ArgumentParser): + raise RuntimeError(f'{parser_name}() returned {type(result).__name__}, expected ArgumentParser') + return result + + if isinstance(attr, argparse.ArgumentParser): + return attr + + raise RuntimeError(f'{parser_name!r} is {type(attr).__name__}, expected a callable or ArgumentParser instance') + + return detect_parser(module) + + +# =============== +# CLI entry point +# =============== + + +class Args(argparse.Namespace): + """ + Annotated :class:`argparse.Namespace` returned by :func:`parse_args`. + """ + script: Path # first positional arg + shell: SupportedShell # second positional arg + parser: str # --parser + name: str # --name + project: str # --project + + +def parse_args(argv: Sequence[str] | None = None) -> Args: + """ + Parse command-line arguments. + + :param argv: argument list to parse, defaults to sys.argv[1:] + :return: annotated namespace of parsed args (:class:`Args`) + """ + parser = argparse.ArgumentParser( + description='Generate shell completion scripts for argparse-based CLI tools.', + formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False, + epilog='\n'.join([ + 'examples:', + ' %(prog)s scripts/maintain/update_prek.py zsh', + ' %(prog)s scripts/maintain/update_prek.py bash --parser build_parser', + ' %(prog)s scripts/maintain/update_prek.py fish --name update_prek', + ]), + ) + + def add_positional_args() -> None: + parser.add_argument( + 'script', + type=Path, + help='path to the target argparse-based script', + ) + parser.add_argument( + 'shell', + choices=get_args(SupportedShell), + help=f'target shell ({", ".join(get_args(SupportedShell))})', + ) + add_positional_args() + + def add_options() -> None: + parser.add_argument( + '--parser', + metavar='NAME', + default=None, + help='name of the function or attribute that provides the ArgumentParser (auto-detected if omitted)', + ) + parser.add_argument( + '--name', + metavar='CMD', + default=None, + help='command name for the completion script (defaults to the script filename)', + ) + parser.add_argument( + '--project', + metavar='NAME', + default=None, + help='project name for header comments', + ) + parser.add_argument( + '-h', + '--help', + action='help', + default=argparse.SUPPRESS, + help='show this help message and exit', + ) + add_options() + + return parser.parse_args(argv, namespace=Args()) + + +def main(argv: Sequence[str] | None = None) -> None: + """ + CLI entry point for generating shell completions from an argparse-based script. + + :param argv: argument list to parse (defaults to ``sys.argv[1:]``) + """ + args = parse_args(argv) + try: + parser = resolve_parser(args.script, parser_name=args.parser) + except (FileNotFoundError, ImportError, RuntimeError) as e: + print(f'error: {e}', file=sys.stderr) + sys.exit(1) + else: + print( + generate_completions( + args.shell, + parser, + command_name=args.name or args.script.name, + script_path=str(args.script), + project_name=args.project or '', + ) + ) + + +if __name__ == '__main__': + main() diff --git a/services/README.md b/services/README.md index 0d44bd7..569dd8c 100644 --- a/services/README.md +++ b/services/README.md @@ -1,101 +1,107 @@ # Services -Docker Compose definitions for the home NAS stack. Each subdirectory contains a self-contained -`compose.yml` for a single service; `compose.base.yml` provides the shared project name and the -`nas-network` bridge that ties them together. +Docker Compose definitions for the home NAS stack. Each subdirectory contains a self-contained `compose.yml` for a +single service; `compose.base.yml` provides the shared project name and the `nas-network` bridge that ties them +together. -These files are deployed to the NAS at `~/docker/` by the `roles/docker` Ansible role and brought -up by `roles/docker/files/deploy.sh`, which discovers every subdirectory containing a `compose.yml` -and runs `docker compose -f compose.base.yml -f /compose.yml up -d `. +These files are deployed to the NAS at `~/docker/` by the `roles/docker` Ansible role and brought up by +`roles/docker/files/deploy.sh`, which discovers every subdirectory containing a `compose.yml` and runs +`docker compose -f compose.base.yml -f /compose.yml up -d `. ## Quick Reference -All host ports below are bound on the NAS itself. Services without a host port are reachable only -from inside the `nas-network` bridge (or, for Home Assistant, the host network namespace). - -| Container | Image | Host Port(s) | Purpose | -|------------------|------------------------------------|-----------------------------------------------|-----------------------------------------------| -| `caddy` | `caddy:2-alpine` | `443/tcp`, `32443/tcp` | Tailscale HTTPS reverse proxy (see below) | -| `glance` | `glanceapp/glance` | `8080/tcp` | Dashboard / homepage | -| `home-assistant` | `homeassistant/home-assistant` | host networking (default `8123/tcp`) | Home automation hub | -| `pihole` | `pihole/pihole` | `53/tcp`, `53/udp`, `67/udp`, `8053/tcp` → 80 | DNS, ad-blocking, DHCP | -| `plex` | `linuxserver/plex` | `32400/tcp` | Media streaming server | -| `jellyfin` | `linuxserver/jellyfin` | `8096/tcp`, `7359/udp` | Media streaming server (Plex alternative) | -| `prowlarr` | `linuxserver/prowlarr` | `9696/tcp` | Indexer aggregator for Sonarr/Radarr | -| `radarr` | `linuxserver/radarr` | `7878/tcp` | Movie library management | -| `sonarr` | `linuxserver/sonarr` | `8989/tcp` | TV library management | -| `transmission` | `linuxserver/transmission` | `9091/tcp` | BitTorrent client (web UI) | -| `flaresolverr` | `ghcr.io/flaresolverr/flaresolverr`| internal only (`8191/tcp` on the bridge) | Cloudflare challenge solver for Prowlarr | -| `autoplex` | `danielmmetz/autoplex` (built) | none | Copies completed downloads into Plex layout | - -LAN access is `http://:` (e.g. `http://nas.local:8989` for Sonarr). The PiHole -admin UI is `http://:8053/admin`. Home Assistant listens directly on the host network at -port `8123`. +All host ports below are bound on the NAS itself. Services without a host port are reachable only from inside the +`nas-network` bridge (or, for Home Assistant, the host network namespace). + +| Container | Image | Host Port(s) | Purpose | +| ---------------- | ----------------------------------- | --------------------------------------------- | ------------------------------------------- | +| `caddy` | `caddy:2-alpine` | `443/tcp`, `32443/tcp` | Tailscale HTTPS reverse proxy (see below) | +| `glance` | `glanceapp/glance` | `8080/tcp` | Dashboard / homepage | +| `home-assistant` | `homeassistant/home-assistant` | host networking (default `8123/tcp`) | Home automation hub | +| `pihole` | `pihole/pihole` | `53/tcp`, `53/udp`, `67/udp`, `8053/tcp` → 80 | DNS, ad-blocking, DHCP | +| `plex` | `linuxserver/plex` | `32400/tcp` | Media streaming server | +| `jellyfin` | `linuxserver/jellyfin` | `8096/tcp`, `7359/udp` | Media streaming server (Plex alternative) | +| `prowlarr` | `linuxserver/prowlarr` | `9696/tcp` | Indexer aggregator for Sonarr/Radarr | +| `radarr` | `linuxserver/radarr` | `7878/tcp` | Movie library management | +| `sonarr` | `linuxserver/sonarr` | `8989/tcp` | TV library management | +| `transmission` | `linuxserver/transmission` | `9091/tcp` | BitTorrent client (web UI) | +| `flaresolverr` | `ghcr.io/flaresolverr/flaresolverr` | internal only (`8191/tcp` on the bridge) | Cloudflare challenge solver for Prowlarr | +| `autoplex` | `danielmmetz/autoplex` (built) | none | Copies completed downloads into Plex layout | + +LAN access is `http://:` (e.g. `http://nas.local:8989` for Sonarr). The PiHole admin UI is +`http://:8053/admin`. Home Assistant listens directly on the host network at port `8123`. ## Tailscale / HTTPS -`caddy` terminates TLS using certs issued by `tailscale cert` and proxies a subset of the services -under a single hostname. Configuration is templated by `roles/tailscale/templates/Caddyfile.j2` -from the `tailscale_services` variable in `inventory/host_vars/nas/vars.yml`. +`caddy` terminates TLS using certs issued by `tailscale cert` and proxies a subset of the services under a single +hostname. Configuration is templated by `roles/tailscale/templates/Caddyfile.j2` from the `tailscale_services` variable +in `inventory/host_vars/nas/vars.yml`. -| Service | External URL | Notes | -|---------------|------------------------------------------------|----------------------------------------------------| -| Sonarr | `https://nas..ts.net/sonarr` | Requires `UrlBase = /sonarr` in app settings | -| Radarr | `https://nas..ts.net/radarr` | Requires `UrlBase = /radarr` in app settings | -| Prowlarr | `https://nas..ts.net/prowlarr` | Requires `UrlBase = /prowlarr` in app settings | -| Transmission | `https://nas..ts.net/transmission` | Requires `rpc-url = /transmission/` in settings | -| PiHole | `https://nas..ts.net/pihole` | Caddy rewrites `/pihole` → `/admin` | -| Glance | `https://nas..ts.net/glance` | Prefix stripped before proxying | -| Plex | `https://nas..ts.net:32443` | Dedicated port; `ADVERTISE_IP` must match | +| Service | External URL | Notes | +| ------------ | ------------------------------------------- | ----------------------------------------------- | +| Sonarr | `https://nas..ts.net/sonarr` | Requires `UrlBase = /sonarr` in app settings | +| Radarr | `https://nas..ts.net/radarr` | Requires `UrlBase = /radarr` in app settings | +| Prowlarr | `https://nas..ts.net/prowlarr` | Requires `UrlBase = /prowlarr` in app settings | +| Transmission | `https://nas..ts.net/transmission` | Requires `rpc-url = /transmission/` in settings | +| PiHole | `https://nas..ts.net/pihole` | Caddy rewrites `/pihole` → `/admin` | +| Glance | `https://nas..ts.net/glance` | Prefix stripped before proxying | +| Plex | `https://nas..ts.net:32443` | Dedicated port; `ADVERTISE_IP` must match | ## Services ### `caddy` -Reverse proxy fronting the stack on the Tailscale interface. Reads `Caddyfile` and TLS material -from `/etc/caddy/` and `/etc/tailscale/certs/` on the host, both populated by the `tailscale` role. + +Reverse proxy fronting the stack on the Tailscale interface. Reads `Caddyfile` and TLS material from `/etc/caddy/` and +`/etc/tailscale/certs/` on the host, both populated by the `tailscale` role. ### `glance` -Web dashboard. Mounts `glance/config` and `glance/assets`, plus the Docker socket (read-only) so it -can display container status. Loads environment from a sibling `.env` file rendered by Ansible. + +Web dashboard. Mounts `glance/config` and `glance/assets`, plus the Docker socket (read-only) so it can display +container status. Loads environment from a sibling `.env` file rendered by Ansible. ### `home-assistant` -Runs on the host network (`network_mode: host`) so it can discover devices via mDNS/SSDP and reach -the Lutron Caseta bridge directly. Secrets, certificates, and YAML config live under -`homeassistant/config/`, populated during the `docker` role's `homeassistant` task. + +Runs on the host network (`network_mode: host`) so it can discover devices via mDNS/SSDP and reach the Lutron Caseta +bridge directly. Secrets, certificates, and YAML config live under `homeassistant/config/`, populated during the +`docker` role's `homeassistant` task. ### `pihole` -DNS server and DHCP-capable ad-blocker. Binds privileged DNS/DHCP ports on the host; the admin UI -is remapped to host port `8053` to avoid colliding with other web services. `FTLCONF_webserver_api_password` -is sourced from the environment (the deploy `.env` file). + +DNS server and DHCP-capable ad-blocker. Binds privileged DNS/DHCP ports on the host; the admin UI is remapped to host +port `8053` to avoid colliding with other web services. `FTLCONF_webserver_api_password` is sourced from the environment +(the deploy `.env` file). ### `plex` -Media server. Uses the LinuxServer.io image with `PUID=1000/PGID=1000`. `ADVERTISE_IP` is set in the -deploy `.env` to the Tailscale HTTPS URL (`https://nas..ts.net:32443`) so remote clients -get a working external address. + +Media server. Uses the LinuxServer.io image with `PUID=1000/PGID=1000`. `ADVERTISE_IP` is set in the deploy `.env` to +the Tailscale HTTPS URL (`https://nas..ts.net:32443`) so remote clients get a working external address. ### `jellyfin` -Media server running alongside Plex for evaluation. Uses the LinuxServer.io image with -`PUID=1000/PGID=1000` and shares the same `/storage/media/{tv,movies,music}` libraries as Plex. -LAN-only HTTP on port `8096`; client auto-discovery on `7359/udp`. Config persists at -`/storage/media/config/jellyfin`. + +Media server running alongside Plex for evaluation. Uses the LinuxServer.io image with `PUID=1000/PGID=1000` and shares +the same `/storage/media/{tv,movies,music}` libraries as Plex. LAN-only HTTP on port `8096`; client auto-discovery on +`7359/udp`. Config persists at `/storage/media/config/jellyfin`. ### `prowlarr`, `radarr`, `sonarr`, `transmission` -Standard *arr / download stack. All share the `/storage/media` tree on the host so completed -downloads can be picked up by Radarr/Sonarr and reorganized by autoplex. + +Standard \*arr / download stack. All share the `/storage/media` tree on the host so completed downloads can be picked up +by Radarr/Sonarr and reorganized by autoplex. ### `flaresolverr` -Headless-browser sidecar used by Prowlarr to solve Cloudflare challenges. Not exposed to the host; -other containers reach it as `http://flaresolverr:8191` over the `nas-network` bridge. + +Headless-browser sidecar used by Prowlarr to solve Cloudflare challenges. Not exposed to the host; other containers +reach it as `http://flaresolverr:8191` over the `nas-network` bridge. ### `autoplex` + Built from source (`danielmmetz/autoplex`) by the `docker` Ansible role. Watches -`/storage/media/downloads/complete/{tv,movies}` and copies new files into the `tv/` and `movies/` -trees consumed by Plex and Sonarr/Radarr. Talks to `transmission` for completion status. +`/storage/media/downloads/complete/{tv,movies}` and copies new files into the `tv/` and `movies/` trees consumed by Plex +and Sonarr/Radarr. Talks to `transmission` for completion status. ## Deploying -The Ansible bootstrap installs `~/docker/deploy.sh` on the NAS. It is idempotent and used by both -the initial provision and the GitHub Actions CI workflow on push to `main`. +The Ansible bootstrap installs `~/docker/deploy.sh` on the NAS. It is idempotent and used by both the initial provision +and the GitHub Actions CI workflow on push to `main`. ```bash # Deploy everything diff --git a/services/homeassistant/config/README.md b/services/homeassistant/config/README.md index 1eb2fbb..7240a52 100644 --- a/services/homeassistant/config/README.md +++ b/services/homeassistant/config/README.md @@ -1,11 +1,13 @@ # homeassistant + bae5hau5 HomeAssisant config ### Necessary Workarounds -- pylutron appears to have a bug of some sort, preventing the Lutron component from authenticating even after -generating a cert with [scripts/get_lutron_cert.py](scripts/get_lutron_cert.py). Found a workaround in an -[open homeassistant issue](https://github.com/home-assistant/home-assistant/issues/15421#issuecomment-459453030), -which requires changing a line in the pylutron python package: + +- pylutron appears to have a bug of some sort, preventing the Lutron component from authenticating even after generating + a cert with [scripts/get_lutron_cert.py](scripts/get_lutron_cert.py). Found a workaround in an + [open homeassistant issue](https://github.com/home-assistant/home-assistant/issues/15421#issuecomment-459453030), + which requires changing a line in the pylutron python package: ``` quick fix : diff --git a/services/homeassistant/config/scripts/get_lutron_cert.py b/services/homeassistant/config/scripts/get_lutron_cert.py index 955e0d1..a33998f 100644 --- a/services/homeassistant/config/scripts/get_lutron_cert.py +++ b/services/homeassistant/config/scripts/get_lutron_cert.py @@ -97,7 +97,7 @@ oauth_code = re.sub(r'^(.*?code=){0,1}([0-9a-f]*)\s*$', r'\2', redirected_url) if oauth_code == '': - raise Exception('Invalid code') + raise Exception('Invalid code') from None token = requests.post( f'{BASE_URL}oauth/token', @@ -110,7 +110,7 @@ }, ).json() if token['token_type'] != 'bearer': - raise Exception(f'Received invalid token {token}. Try generating a new code (one time use)') + raise Exception(f'Received invalid token {token}. Try generating a new code (one time use)') from None pairing_response = requests.post( f'{BASE_URL}api/v1/remotepairing/application/user', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uv.lock b/uv.lock index e66e332..dbf0bbf 100644 --- a/uv.lock +++ b/uv.lock @@ -1,545 +1,1590 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "ansible-core" +version = "2.21.1" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "resolvelib" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/26/6d/38d7eaea1df4a95c3b137e780f6ad2208d7886334a291cadde5d645f08a1/ansible_core-2.21.1.tar.gz", hash = "sha256:a5536ece95be84de15212b3644cdbbe9cbd9efd62e4e8a544cd6b0b27a083039", size = 3384534, upload-time = "2026-06-18T19:35:02.887Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/90/55/894f2cad10ef650bf33a0e64e242805de2d1a8835f539d04d52053fe08a6/ansible_core-2.21.1-py3-none-any.whl", hash = "sha256:1403ff56bb69484ef90788126d06c65b4e28e8d2dcdcfb7f027d4c1495052d7e", size = 2454057, upload-time = "2026-06-18T19:35:00.741Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://pypi.apple.com/packages/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://pypi.apple.com/packages/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://pypi.apple.com/packages/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://pypi.apple.com/packages/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://pypi.apple.com/packages/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://pypi.apple.com/packages/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://pypi.apple.com/packages/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://pypi.apple.com/packages/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://pypi.apple.com/packages/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://pypi.apple.com/packages/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://pypi.apple.com/packages/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://pypi.apple.com/packages/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://pypi.apple.com/packages/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://pypi.apple.com/packages/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://pypi.apple.com/packages/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://pypi.apple.com/packages/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://pypi.apple.com/packages/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://pypi.apple.com/packages/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://pypi.apple.com/packages/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://pypi.apple.com/packages/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://pypi.apple.com/packages/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://pypi.apple.com/packages/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://pypi.apple.com/packages/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://pypi.apple.com/packages/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://pypi.apple.com/packages/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://pypi.apple.com/packages/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://pypi.apple.com/packages/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://pypi.apple.com/packages/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://pypi.apple.com/packages/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://pypi.apple.com/packages/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://pypi.apple.com/packages/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://pypi.apple.com/packages/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + +[[package]] +name = "astatine" +version = "0.3.3" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "domdf-python-tools" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/a4/8a/544e058e5ed049cffd67006944872216c48e86b9004ceeefbb33a9759d3d/astatine-0.3.3.tar.gz", hash = "sha256:0c58a7844b5890ff16da07dbfeb187341d8324cb4378940f89d795cbebebce08", size = 6687, upload-time = "2023-08-15T10:38:28.151Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/1e/72/58e5d9b9965f55a1b652c7ea73dceedf4f8cea08d1310aa2d4102202acb8/astatine-0.3.3-py3-none-any.whl", hash = "sha256:6d8c914f01fbea252cb8f31563f2e766a9ab03c02b9bcc37d18f7d9138828401", size = 17376, upload-time = "2023-08-15T10:38:26.735Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "26.1.0" -source = { registry = "https://pypi.org/simple" } -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" } +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, + { url = "https://pypi.apple.com/packages/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]] -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" } +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } 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" }, + { url = "https://pypi.apple.com/packages/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://pypi.apple.com/packages/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://pypi.apple.com/packages/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://pypi.apple.com/packages/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://pypi.apple.com/packages/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://pypi.apple.com/packages/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://pypi.apple.com/packages/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://pypi.apple.com/packages/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://pypi.apple.com/packages/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://pypi.apple.com/packages/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://pypi.apple.com/packages/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://pypi.apple.com/packages/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://pypi.apple.com/packages/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://pypi.apple.com/packages/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://pypi.apple.com/packages/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://pypi.apple.com/packages/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://pypi.apple.com/packages/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://pypi.apple.com/packages/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://pypi.apple.com/packages/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://pypi.apple.com/packages/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://pypi.apple.com/packages/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://pypi.apple.com/packages/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://pypi.apple.com/packages/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://pypi.apple.com/packages/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://pypi.apple.com/packages/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://pypi.apple.com/packages/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://pypi.apple.com/packages/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://pypi.apple.com/packages/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://pypi.apple.com/packages/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://pypi.apple.com/packages/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://pypi.apple.com/packages/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://pypi.apple.com/packages/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://pypi.apple.com/packages/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://pypi.apple.com/packages/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://pypi.apple.com/packages/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://pypi.apple.com/packages/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://pypi.apple.com/packages/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://pypi.apple.com/packages/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://pypi.apple.com/packages/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://pypi.apple.com/packages/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://pypi.apple.com/packages/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://pypi.apple.com/packages/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://pypi.apple.com/packages/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://pypi.apple.com/packages/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://pypi.apple.com/packages/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://pypi.apple.com/packages/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://pypi.apple.com/packages/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://pypi.apple.com/packages/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://pypi.apple.com/packages/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://pypi.apple.com/packages/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://pypi.apple.com/packages/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://pypi.apple.com/packages/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://pypi.apple.com/packages/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://pypi.apple.com/packages/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://pypi.apple.com/packages/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://pypi.apple.com/packages/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://pypi.apple.com/packages/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://pypi.apple.com/packages/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://pypi.apple.com/packages/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://pypi.apple.com/packages/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://pypi.apple.com/packages/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://pypi.apple.com/packages/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://pypi.apple.com/packages/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://pypi.apple.com/packages/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://pypi.apple.com/packages/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://pypi.apple.com/packages/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://pypi.apple.com/packages/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://pypi.apple.com/packages/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://pypi.apple.com/packages/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://pypi.apple.com/packages/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://pypi.apple.com/packages/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://pypi.apple.com/packages/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://pypi.apple.com/packages/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://pypi.apple.com/packages/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] [[package]] name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } +version = "8.4.2" +source = { registry = "https://pypi.apple.com/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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.2" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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/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://pypi.apple.com/packages/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]] 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" } +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "consolekit" +version = "1.13.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "click" }, + { name = "deprecation-alias" }, + { name = "domdf-python-tools" }, + { name = "mistletoe" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/60/1f/b1745cfe7b1c32d0cfe76b09a184c0d9360851a92516da0ea3647e39c759/consolekit-1.13.0.tar.gz", hash = "sha256:6c28df284ec86fb395fbe39493ddf9f8dfc8b181a6156abfd50c3f2156ad2b20", size = 32316, upload-time = "2026-02-19T16:12:14.026Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/a8/63/e08e7d4a2a89ab7769ca7e8066d04cbf6620ff4065ee0ea9a47b4cdabae6/consolekit-1.13.0-py3-none-any.whl", hash = "sha256:5b274587d32344dbb7a7aae29df6478e1bbbf6842494f74fe6126fada5ded3ea", size = 46437, upload-time = "2026-02-19T16:12:11.475Z" }, +] + +[[package]] +name = "coverage" +version = "7.15.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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/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" }, + { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "connor-mason-dotfiles" +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://pypi.apple.com/packages/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://pypi.apple.com/packages/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://pypi.apple.com/packages/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://pypi.apple.com/packages/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://pypi.apple.com/packages/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://pypi.apple.com/packages/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://pypi.apple.com/packages/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://pypi.apple.com/packages/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://pypi.apple.com/packages/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://pypi.apple.com/packages/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://pypi.apple.com/packages/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://pypi.apple.com/packages/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://pypi.apple.com/packages/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://pypi.apple.com/packages/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://pypi.apple.com/packages/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://pypi.apple.com/packages/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://pypi.apple.com/packages/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://pypi.apple.com/packages/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://pypi.apple.com/packages/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://pypi.apple.com/packages/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://pypi.apple.com/packages/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://pypi.apple.com/packages/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://pypi.apple.com/packages/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://pypi.apple.com/packages/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://pypi.apple.com/packages/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://pypi.apple.com/packages/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://pypi.apple.com/packages/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://pypi.apple.com/packages/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://pypi.apple.com/packages/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://pypi.apple.com/packages/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://pypi.apple.com/packages/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://pypi.apple.com/packages/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://pypi.apple.com/packages/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://pypi.apple.com/packages/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://pypi.apple.com/packages/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://pypi.apple.com/packages/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://pypi.apple.com/packages/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://pypi.apple.com/packages/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "deprecation-alias" +version = "0.4.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "packaging" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/33/e0/2484747bc4936baf2737bb539b9e5b7f6d3a4c47f22d8b137c0a1fde356b/deprecation_alias-0.4.0.tar.gz", hash = "sha256:a58d2e74491c7834e9d318788da60272fa2f81aeb814b4055a4ba90a0a41740f", size = 10898, upload-time = "2025-02-11T15:53:59.001Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/a2/00/a0514192a926e2c077a170f9f58653a0b3dd2ff034fdee48da843ea01d6c/deprecation_alias-0.4.0-py3-none-any.whl", hash = "sha256:a2d3cb08705d81bcc845ebbeff8e981ca7e5ef6c51478f0c771c38a54d7d7811", size = 13768, upload-time = "2025-02-11T15:53:57.83Z" }, +] + +[[package]] +name = "domdf-python-tools" +version = "3.10.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "natsort" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/36/8b/ab2d8a292bba8fe3135cacc8bfd3576710a14b8f2d0a8cde19130d5c9d21/domdf_python_tools-3.10.0.tar.gz", hash = "sha256:2ae308d2f4f1e9145f5f4ba57f840fbfd1c2983ee26e4824347789649d3ae298", size = 100458, upload-time = "2025-02-12T17:34:05.747Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/5b/11/208f72084084d3f6a2ed5ebfdfc846692c3f7ad6dce65e400194924f7eed/domdf_python_tools-3.10.0-py3-none-any.whl", hash = "sha256:5e71c1be71bbcc1f881d690c8984b60e64298ec256903b3147f068bc33090c36", size = 126860, upload-time = "2025-02-12T17:34:04.093Z" }, +] + +[[package]] +name = "dotfiles" version = "0.0.1" source = { virtual = "." } dependencies = [ + { name = "ansible-core" }, + { name = "pyyaml" }, { name = "typing-extensions" }, ] [package.dev-dependencies] dev = [ + { name = "codespell" }, + { name = "coverage" }, + { name = "flake8-dunder-all" }, { name = "interrogate" }, + { name = "mdformat" }, + { name = "mdformat-config" }, + { name = "mdformat-footnote" }, + { name = "mdformat-front-matters" }, + { name = "mdformat-gfm" }, + { name = "mdformat-gfm-alerts" }, + { name = "mdformat-pyproject" }, + { name = "mdformat-simple-breaks" }, + { name = "mdformat-toc" }, { name = "mypy" }, - { name = "pre-commit" }, + { name = "prek" }, + { name = "pyjson5" }, + { name = "pytest" }, + { name = "pytest-clarity" }, + { name = "pytest-html" }, + { name = "pytest-mock" }, + { name = "pytest-sugar" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "pyyaml" }, { name = "ruff" }, + { name = "shellcheck-py" }, + { name = "shfmt-py" }, + { name = "taplo" }, { name = "types-pyyaml" }, { name = "types-requests" }, + { name = "types-toml" }, + { name = "validate-pyproject", extra = ["all"] }, + { name = "validate-pyproject-schema-store" }, + { name = "yamllint" }, ] lint = [ + { name = "codespell" }, + { name = "flake8-dunder-all" }, { name = "interrogate" }, + { name = "mdformat" }, + { name = "mdformat-config" }, + { name = "mdformat-footnote" }, + { name = "mdformat-front-matters" }, + { name = "mdformat-gfm" }, + { name = "mdformat-gfm-alerts" }, + { name = "mdformat-pyproject" }, + { name = "mdformat-simple-breaks" }, + { name = "mdformat-toc" }, { name = "mypy" }, - { name = "pre-commit" }, + { name = "prek" }, + { name = "pyjson5" }, { name = "ruff" }, + { name = "shellcheck-py" }, + { name = "shfmt-py" }, + { name = "taplo" }, { name = "types-pyyaml" }, { name = "types-requests" }, + { name = "types-toml" }, + { name = "validate-pyproject", extra = ["all"] }, + { name = "validate-pyproject-schema-store" }, + { name = "yamllint" }, +] +test = [ + { name = "coverage" }, + { name = "pytest" }, + { name = "pytest-clarity" }, + { name = "pytest-html" }, + { name = "pytest-mock" }, + { name = "pytest-sugar" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "pyyaml" }, + { name = "yamllint" }, ] [package.metadata] -requires-dist = [{ name = "typing-extensions" }] +requires-dist = [ + { name = "ansible-core" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "typing-extensions" }, +] [package.metadata.requires-dev] dev = [ + { name = "codespell" }, + { name = "coverage", extras = ["toml"], specifier = ">=7.11" }, + { name = "flake8-dunder-all" }, { name = "interrogate", specifier = ">=1.7.0" }, - { name = "mypy", specifier = ">=1.19.1" }, - { name = "pre-commit" }, + { name = "mdformat" }, + { name = "mdformat-config" }, + { name = "mdformat-footnote" }, + { name = "mdformat-front-matters" }, + { name = "mdformat-gfm" }, + { name = "mdformat-gfm-alerts" }, + { name = "mdformat-pyproject" }, + { name = "mdformat-simple-breaks" }, + { name = "mdformat-toc" }, + { name = "mypy", marker = "python_full_version < '3.10'", specifier = ">=1.19.1,<1.20" }, + { name = "mypy", marker = "python_full_version >= '3.10'", specifier = ">=2.0" }, + { name = "prek" }, + { name = "pyjson5" }, + { name = "pytest", specifier = ">=9.0" }, + { name = "pytest-clarity", specifier = ">=1.0.1" }, + { name = "pytest-html", specifier = ">=3.2.0" }, + { name = "pytest-mock", specifier = ">=3.11.1" }, + { name = "pytest-sugar", specifier = ">=0.9.7" }, + { name = "pytest-timeout", specifier = ">=2.1.0" }, + { name = "pytest-xdist", specifier = ">=3.3.0" }, + { name = "pyyaml", specifier = ">=6.0.1" }, { name = "ruff", specifier = ">=0.15.8" }, + { name = "shellcheck-py" }, + { name = "shfmt-py" }, + { name = "taplo" }, { name = "types-pyyaml" }, { name = "types-requests" }, + { name = "types-toml" }, + { name = "validate-pyproject", extras = ["all"] }, + { name = "validate-pyproject-schema-store" }, + { name = "yamllint" }, + { name = "yamllint", specifier = ">=1.32.0" }, ] lint = [ + { name = "codespell" }, + { name = "flake8-dunder-all" }, { name = "interrogate", specifier = ">=1.7.0" }, - { name = "mypy", specifier = ">=1.19.1" }, - { name = "pre-commit" }, + { name = "mdformat" }, + { name = "mdformat-config" }, + { name = "mdformat-footnote" }, + { name = "mdformat-front-matters" }, + { name = "mdformat-gfm" }, + { name = "mdformat-gfm-alerts" }, + { name = "mdformat-pyproject" }, + { name = "mdformat-simple-breaks" }, + { name = "mdformat-toc" }, + { name = "mypy", marker = "python_full_version < '3.10'", specifier = ">=1.19.1,<1.20" }, + { name = "mypy", marker = "python_full_version >= '3.10'", specifier = ">=2.0" }, + { name = "prek" }, + { name = "pyjson5" }, { name = "ruff", specifier = ">=0.15.8" }, + { name = "shellcheck-py" }, + { name = "shfmt-py" }, + { name = "taplo" }, { name = "types-pyyaml" }, { name = "types-requests" }, + { name = "types-toml" }, + { name = "validate-pyproject", extras = ["all"] }, + { name = "validate-pyproject-schema-store" }, + { name = "yamllint" }, +] +test = [ + { name = "coverage", extras = ["toml"], specifier = ">=7.11" }, + { name = "pytest", specifier = ">=9.0" }, + { name = "pytest-clarity", specifier = ">=1.0.1" }, + { name = "pytest-html", specifier = ">=3.2.0" }, + { name = "pytest-mock", specifier = ">=3.11.1" }, + { name = "pytest-sugar", specifier = ">=0.9.7" }, + { name = "pytest-timeout", specifier = ">=2.1.0" }, + { name = "pytest-xdist", specifier = ">=3.3.0" }, + { name = "pyyaml", specifier = ">=6.0.1" }, + { name = "yamllint", specifier = ">=1.32.0" }, ] [[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" } +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } 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://pypi.apple.com/packages/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] -name = "filelock" -version = "3.25.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, + { url = "https://pypi.apple.com/packages/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 = "identify" -version = "2.6.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, + { url = "https://pypi.apple.com/packages/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[package]] +name = "flake8-dunder-all" +version = "0.5.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "astatine" }, + { name = "click" }, + { name = "consolekit" }, + { name = "domdf-python-tools" }, + { name = "flake8" }, + { name = "natsort" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/69/94/c6fe6c1ea239d6e732beb38324bcecf0132008260183caf2a2d31bc9565a/flake8_dunder_all-0.5.0.tar.gz", hash = "sha256:4ea582eb77cc7333df223608cb4d6ae813f07b167c19d4d65a6a52e388e89398", size = 9716, upload-time = "2025-05-21T16:26:40.723Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/e4/6c/e7c3401ce8c5515b80bd121f89bb0fa241cc3f7a0c0a83b38be2a56d2d83/flake8_dunder_all-0.5.0-py3-none-any.whl", hash = "sha256:fcd7d4ac9cbb3a1bca4c41221a6eb8bcd3c0965269e98772802cdd333beb7adc", size = 12153, upload-time = "2025-05-21T16:26:39.332Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "interrogate" version = "1.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pypi.apple.com/simple" } dependencies = [ { name = "attrs" }, { name = "click" }, { name = "colorama" }, { name = "py" }, { name = "tabulate" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/22/74f7fcc96280eea46cf2bcbfa1354ac31de0e60a4be6f7966f12cef20893/interrogate-1.7.0.tar.gz", hash = "sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0", size = 159636, upload-time = "2024-04-07T22:30:46.217Z" } +sdist = { url = "https://pypi.apple.com/packages/packages/8b/22/74f7fcc96280eea46cf2bcbfa1354ac31de0e60a4be6f7966f12cef20893/interrogate-1.7.0.tar.gz", hash = "sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0", size = 159636, upload-time = "2024-04-07T22:30:46.217Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl", hash = "sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12", size = 46982, upload-time = "2024-04-07T22:30:44.277Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://pypi.apple.com/packages/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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl", hash = "sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12", size = 46982, upload-time = "2024-04-07T22:30:44.277Z" }, + { url = "https://pypi.apple.com/packages/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 = "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/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, - { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, - { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, - { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, - { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, - { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, - { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, - { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, - { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, - { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, - { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, - { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, - { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, - { 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.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdformat" +version = "1.0.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/3f/05/32b5e14b192b0a8a309f32232c580aefedd9d06017cb8fe8fce34bec654c/mdformat-1.0.0.tar.gz", hash = "sha256:4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928", size = 56953, upload-time = "2025-10-16T12:05:03.695Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/54/9a/8fe71b95985ca7a4001effbcc58e5a07a1f2a2884203f74dcf48a3b08315/mdformat-1.0.0-py3-none-any.whl", hash = "sha256:bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b", size = 53288, upload-time = "2025-10-16T12:05:02.607Z" }, +] + +[[package]] +name = "mdformat-config" +version = "0.2.1" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "ruamel-yaml" }, + { name = "taplo" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/ed/c0/49baad1d5121b1b7f30b5166f668d70d2a16ce5d2c267ebe7f210f575b59/mdformat_config-0.2.1.tar.gz", hash = "sha256:753aa1179198c791e4791099a0e9b1684a649fbbcd52db7c4d2e911a4c2c69ae", size = 2829, upload-time = "2024-10-18T19:42:53.522Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/4e/73/e989491953da2cb446253258d0f3694ea021c277ca0cfbefe0a79ca5eef1/mdformat_config-0.2.1-py3-none-any.whl", hash = "sha256:484a867c067366f85c9e4b91e36d8a92c80141490db59e481befe0ec8055f02d", size = 3473, upload-time = "2024-10-18T19:42:51.998Z" }, +] + +[[package]] +name = "mdformat-footnote" +version = "0.1.3" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdit-py-plugins" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/3f/c0/0fe461e3c53eb72b35b642add6c3eaf1f90e43a574a18633955f9e89791d/mdformat_footnote-0.1.3.tar.gz", hash = "sha256:70617e61af87f59d7dea93a392c5093089a6d4551126fa18025c6fddb18bf742", size = 6430, upload-time = "2026-01-30T14:13:05.983Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/7e/ac/60d47237a0e37fe991cbf311f10f096a28cfdbbe25d1932dadf8e9abdac8/mdformat_footnote-0.1.3-py3-none-any.whl", hash = "sha256:3033184237cbaf2a71cc02a4c37f043a97c1f16d3ed6939bc104964e77aad0b3", size = 7767, upload-time = "2026-01-30T14:13:04.944Z" }, +] + +[[package]] +name = "mdformat-front-matters" +version = "2.0.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "ruamel-yaml" }, + { name = "toml" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/2c/53/008805510d3006d9aec6c602fa40305750cb96daba8ea9abae477149909b/mdformat_front_matters-2.0.0.tar.gz", hash = "sha256:46efabe93707699120c67d4b33b51fdb4cfe0e040acfb0f8212eb33c9a3e8a9a", size = 12576, upload-time = "2025-12-04T11:44:52.265Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/00/05/4c197c000dab41f1f58290221a4c02bd57804ff42e9374d3dd0f5408aa40/mdformat_front_matters-2.0.0-py3-none-any.whl", hash = "sha256:8ea92d23d1e9427fe6548b9044e5fbf7bcf6c217941f1a9442e0efd8b0a76e61", size = 13411, upload-time = "2025-12-04T11:44:51.209Z" }, +] + +[[package]] +name = "mdformat-gfm" +version = "1.0.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "wcwidth" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/56/6f/a626ebb142a290474401b67e2d61e73ce096bf7798ee22dfe6270f924b3f/mdformat_gfm-1.0.0.tar.gz", hash = "sha256:d1d49a409a6acb774ce7635c72d69178df7dce1dc8cdd10e19f78e8e57b72623", size = 10112, upload-time = "2025-10-16T09:12:22.402Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/e6/18/6bc2189b744dd383cad03764f41f30352b1278d2205096f77a29c0b327ad/mdformat_gfm-1.0.0-py3-none-any.whl", hash = "sha256:7305a50efd2a140d7c83505b58e3ac5df2b09e293f9bbe72f6c7bee8c678b005", size = 10970, upload-time = "2025-10-16T09:12:21.276Z" }, +] + +[[package]] +name = "mdformat-gfm-alerts" +version = "2.0.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdit-py-plugins" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/c7/33/70619cf9e2b9e35857ebcf7e947e8f5370df2ff2a4ab666895783f8fab6c/mdformat_gfm_alerts-2.0.0.tar.gz", hash = "sha256:eb2b3189ad44ae28a6b6b714609dd3a30d6e3b898f02b1e6473d7b08df8bb3c0", size = 9687, upload-time = "2025-06-05T20:49:46.414Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/89/ea/22e093bb2ef3e25dc3ccc9d8392c14b069a007ae394a12e058d9e1f51c00/mdformat_gfm_alerts-2.0.0-py3-none-any.whl", hash = "sha256:e003422cc003bc6e936d0797553f23201095a1d1e8602c5062296d223f2ae516", size = 6752, upload-time = "2025-06-05T20:49:45.539Z" }, +] + +[[package]] +name = "mdformat-pyproject" +version = "0.1.1" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "mdformat" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/a7/33/4c7400f8d2ed273d675e72f55c33e72e08e6224a35f184ffca8ae6d40d53/mdformat_pyproject-0.1.1.tar.gz", hash = "sha256:be16ed754d42db5f5e376ccb9cdbdfefc7750f7f77eadddde37f99ab87272b54", size = 5654, upload-time = "2025-10-29T16:10:24.651Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/5a/31/113382823075056a46de42b21ed31372908f30d353c11ca01ec447ca2829/mdformat_pyproject-0.1.1-py3-none-any.whl", hash = "sha256:92d849f5c587ec7dc74a8d240139d3a9ecb65ea553cdb11e01f927fc55f248ce", size = 5102, upload-time = "2025-10-29T16:10:23.124Z" }, +] + +[[package]] +name = "mdformat-simple-breaks" +version = "0.1.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "mdformat" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/97/cf/21faa8db8ac7548fc58424a82a48b4e6c53dcd45d4cf86494586e8eb4329/mdformat_simple_breaks-0.1.0.tar.gz", hash = "sha256:0f1d57edba17d6ca7175031a3f2edb2e69ea5800c7c406c905e81e75a325470d", size = 6684, upload-time = "2025-10-28T07:06:14.34Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/c1/61/ae77d87133bd32c86631dcfb595a24432cec45d868c99b7e9b6b50597bf0/mdformat_simple_breaks-0.1.0-py3-none-any.whl", hash = "sha256:c81c3d2ee65f0a0d9a968fb02347d1d34a14a6b0f3655c01e2a05bba10e8211f", size = 4265, upload-time = "2025-10-28T07:06:13.481Z" }, +] + +[[package]] +name = "mdformat-toc" +version = "0.5.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "mdformat" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/e8/64/023dac7c53aa2c915838a4ce52fbb724db41635b559024a943cb2f036586/mdformat_toc-0.5.0.tar.gz", hash = "sha256:28e87519fe58d5f25133ba27998bfcba46f5710a40df9e4a19da787e03088d52", size = 7934, upload-time = "2025-10-15T21:30:33.633Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/71/c9/8bce9c6a97c4968d0381d09ae8b8e5a9d8dea292896f37c898fe87da4a71/mdformat_toc-0.5.0-py3-none-any.whl", hash = "sha256:0d26e222db39e3fe7527644cf3d99f83cc8f66295156855e46b054bc4636b328", size = 9711, upload-time = "2025-10-15T21:30:31.495Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "mistletoe" +version = "1.5.1" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/31/ae/d33647e2a26a8899224f36afc5e7b7a670af30f1fd87231e9f07ca19d673/mistletoe-1.5.1.tar.gz", hash = "sha256:c5571ce6ca9cfdc7ce9151c3ae79acb418e067812000907616427197648030a3", size = 111769, upload-time = "2025-12-07T16:19:01.066Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/20/60/0980fefdc4d12c18c1bbab9d62852f27aded8839233c7b0a9827aaf395f5/mistletoe-1.5.1-py3-none-any.whl", hash = "sha256:d3e97664798261503f685f6a6281b092628367cf3128fc68a015a993b0c4feb3", size = 55331, upload-time = "2025-12-07T16:18:59.65Z" }, ] [[package]] name = "mypy" -version = "1.19.1" -source = { registry = "https://pypi.org/simple" } +version = "2.2.0" +source = { registry = "https://pypi.apple.com/simple" } dependencies = [ + { name = "ast-serialize" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { 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/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, - { 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://pypi.apple.com/packages/packages/e9/7e/be536678c6ae49ef058aba4b483d8c7bc104f471479016066f345bc1f5f8/mypy-2.2.0.tar.gz", hash = "sha256:2cdd99d48590dce6f6b7f1961eda75386364398fcdaad86923bc0f0231bf9baf", size = 3950939, upload-time = "2026-07-08T01:37:27.335Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/d1/be/fbaba7b0ee89874fb11668416dec9e5585c190b676b0796cff26a9290fe8/mypy-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:484a2be712b245ac6e89847141f1f50c612b0a924aa25917e63e6cfcf4da07cd", size = 14928025, upload-time = "2026-07-08T01:37:24.376Z" }, + { url = "https://pypi.apple.com/packages/packages/c5/8f/f79a7c5a76671b0f563d4beaa7d99fe90df4500d2c1d2ba1be0432121bcf/mypy-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb0a020dc68480d40e484675558ed637140df1ccbf896a81ba68bca85f2b50a0", size = 13793027, upload-time = "2026-07-08T01:34:33.937Z" }, + { url = "https://pypi.apple.com/packages/packages/b2/b9/3db0086bab611d34e26061b86189e6f71de6d22a9b81699a93b006eabcf6/mypy-2.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc361340732ce7108fa0308812caf02bb6868f16112f1efd35bcad88badf3327", size = 14108322, upload-time = "2026-07-08T01:36:08.316Z" }, + { url = "https://pypi.apple.com/packages/packages/58/29/4f1e13979a848de2a0fd385462354b58358b6e8b3d9661663e308f6e3d5d/mypy-2.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0179a3a0b833f724a65f22613607cf7ea941ab17ec34fa283f8d6dfe21d9fa9", size = 15190198, upload-time = "2026-07-08T01:36:20.168Z" }, + { url = "https://pypi.apple.com/packages/packages/14/f7/7759f6294d9d25d86671957d0974a215a2a24d429526e26a2f603de951c5/mypy-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e0899b13da1e4ba44b880550f247402ce90ffecc71c54b220bcbe7ecb34f394", size = 15424222, upload-time = "2026-07-08T01:33:39.398Z" }, + { url = "https://pypi.apple.com/packages/packages/d7/1b/05b212bef4d2234b5f0b551ea53ce0680d8075b2e79861c765f70b590945/mypy-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:511320b17467402e2906130e185abffffa3d7648aff1444fc2abb61f4c8a087d", size = 11135191, upload-time = "2026-07-08T01:35:03.019Z" }, + { url = "https://pypi.apple.com/packages/packages/92/51/495e7122f6589948b36d3820a046461906756a0eb1b6dedc13ebfec7815e/mypy-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:7589f370b33dcdd95708f5340f13a67c2c49140957f934b42ef63064d343cca0", size = 10132502, upload-time = "2026-07-08T01:34:04.508Z" }, + { url = "https://pypi.apple.com/packages/packages/a3/5f/2d7a9ac5646274cd6e77ce3abcc2a9ece760c2b21f4c4b9f301711e07855/mypy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6968f27347ef539c443ddfd6897e79db525ddb8c856aa8fbf14c34f310ca5193", size = 14931618, upload-time = "2026-07-08T01:33:51.702Z" }, + { url = "https://pypi.apple.com/packages/packages/b2/8a/1adaa7caaa104f87021b1ac71252d62e646e9b623d77900ac7a0ae252bf3/mypy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3df226d2a0ae2c3b03845af217800a68e2965d4b14914c99b78d3a2c8ae23299", size = 13857718, upload-time = "2026-07-08T01:35:27.555Z" }, + { url = "https://pypi.apple.com/packages/packages/1d/15/b11586b5aebbb82213e297fc30a6fcf3bed6a9deea3739cd8dd87621f3fd/mypy-2.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc53553996aca2094216ad9306a6f06c5265d206c1bcb54dd367560bd3557825", size = 14059704, upload-time = "2026-07-08T01:33:57.363Z" }, + { url = "https://pypi.apple.com/packages/packages/03/db/071e05ab442596bdf7a845e830d5ef7128a0175281038245b171a6b16873/mypy-2.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:996abf2f0bebf572556c60720e8dc0cf5292b64060fa68d7f2bc9caa48e01b6f", size = 15128719, upload-time = "2026-07-08T01:33:33.855Z" }, + { url = "https://pypi.apple.com/packages/packages/f3/1c/c4b84eafb85ee315da72471523cc1bf7d7c42164085c42333601da7a8817/mypy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ec287c2381898c652bf8ff79448627fe1a9ee76d22005181fac7315a485c7108", size = 15378692, upload-time = "2026-07-08T01:34:10.468Z" }, + { url = "https://pypi.apple.com/packages/packages/68/a4/59a0ee94877fdfe2958cec9b6add72a75393063c79cb60ab4026dd5e10c2/mypy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2c967a7685fa93fcf1a778b3ebe76e756b28ba14655f539d7b61ff3da69352a", size = 11150911, upload-time = "2026-07-08T01:35:21.603Z" }, + { url = "https://pypi.apple.com/packages/packages/90/e4/6a9144be50180ed43d8c92de9b03dff504daa92b5bcc0353e8960799a23b/mypy-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:d59a4b80351ec92e5f7415fcdd008bd77fcbefc7adf9cbc7ffe4eb9f71617734", size = 10125389, upload-time = "2026-07-08T01:36:36.164Z" }, + { url = "https://pypi.apple.com/packages/packages/73/32/0aa8d8d197023ca6040f7b25a486cb47037b6350b0d3bae657c8f85fb43f/mypy-2.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1f6c3d76853071409ac58fc0aadfb276a22af5f190fdaa02152a858088a39ebd", size = 14926083, upload-time = "2026-07-08T01:36:02.365Z" }, + { url = "https://pypi.apple.com/packages/packages/e2/7c/35bbe0cb10e6699f90e988e537aaf4282a6c16e37f58848a242eb0a98bde/mypy-2.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bdaa177e80cc3292824d4ef3670b5b58771ee8d57c290e0c9c89e7968212332c", size = 13879985, upload-time = "2026-07-08T01:36:31.093Z" }, + { url = "https://pypi.apple.com/packages/packages/f9/0c/1597fbebd873e9b63452317740ae3dd32692cec5da180cc65acd96cd28cf/mypy-2.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80923e6d6e7878291f537ee11052f974954c20cb569798429a5dc265eb780b47", size = 14076883, upload-time = "2026-07-08T01:34:21.789Z" }, + { url = "https://pypi.apple.com/packages/packages/68/35/2ec021a83ec01b5d522639f78d8b36adade7fa4821db0f48fd6d82e861f3/mypy-2.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f24bd465a09077c8d64be8f19a6646db467a55490fd315fe7871afe6bb9645", size = 15103567, upload-time = "2026-07-08T01:36:47.717Z" }, + { url = "https://pypi.apple.com/packages/packages/53/3a/8cb3529f6d6800c7d069935e5c83a05d80263847b8a947cf6b0b16a9e958/mypy-2.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:db34595464869f474708e769413d1d739fc33a69850f253757b9a4cc20bc1fec", size = 15354641, upload-time = "2026-07-08T01:36:41.592Z" }, + { url = "https://pypi.apple.com/packages/packages/d6/4d/320bc9a9553f8a9db5e847ec5ded762ef7ed7403c76c4ba2e8181c80e2f0/mypy-2.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b48092132c7b0ef4322773fecae62fc5b0bc339be348badeec8af502122a4a51", size = 7694355, upload-time = "2026-07-08T01:37:03.291Z" }, + { url = "https://pypi.apple.com/packages/packages/90/05/bf3b349e2f885cd3aab488111bb9049439c28bc028dac5073350d3df8fbe/mypy-2.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:6fc0e98b95e31755ca06d89f75fafa7820fbb3ea2caace6d83cba17625cd0acb", size = 11329146, upload-time = "2026-07-08T01:35:38.318Z" }, + { url = "https://pypi.apple.com/packages/packages/41/a5/558b06e6cfe17ab88bb38f7b370b6bc68a74ba177c9e138db9748e422d2d/mypy-2.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:bc73a5b4d40e8a3e6b12ef82eb0c90430964e34016a36c2aff4e3bfe37ba41f0", size = 10316586, upload-time = "2026-07-08T01:36:25.458Z" }, + { url = "https://pypi.apple.com/packages/packages/0b/21/f0b96f19a9b8ba111a45ffbe9508e818b7f6990469b38f6888943f7bfd3a/mypy-2.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6257bd4b4c0ae2148548b869a1ff3758e38645b92c8fe65eca401866c3c551c1", size = 15922565, upload-time = "2026-07-08T01:35:56.282Z" }, + { url = "https://pypi.apple.com/packages/packages/18/f2/1dbcb20b0865d5e992541450a8c73f2fcc90f8bd7d8a4b81313e16934870/mypy-2.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dfa22b3ae862ac1ce76f5976ddd402651b5f090bcfd49c6d0484b8983a29eaf8", size = 14816515, upload-time = "2026-07-08T01:34:58.167Z" }, + { url = "https://pypi.apple.com/packages/packages/84/a2/18cce9c7d5b4d14010d1f13836da11b234dda917b17ca8671fc32c136997/mypy-2.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30a430bf26fe8cf372f3933fbd83e633d6561868803645a20e4e6d4523f52a3b", size = 15272246, upload-time = "2026-07-08T01:37:08.72Z" }, + { url = "https://pypi.apple.com/packages/packages/ce/71/24d720c7924829bd675cbde2d0fa779f50abf676ca617f53d6a8bfef5fa7/mypy-2.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d58655a60e823b1a4c9ebcda072897fb0193c2f3e6f8e7c433e152aa4cb00233", size = 16226295, upload-time = "2026-07-08T01:34:51.861Z" }, + { url = "https://pypi.apple.com/packages/packages/b7/5e/785730990fc863ad8340b4ab44ac4ca23270aecff92c180ccdf27f9f5869/mypy-2.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d4452c955caf14e28bb046cbd0c3671272e6381630a8b81b0da9713558148890", size = 16493275, upload-time = "2026-07-08T01:35:09.337Z" }, + { url = "https://pypi.apple.com/packages/packages/93/33/55b1edf16f639f153972380d6977b81f65509c5b8f9c86b58b94b7990b03/mypy-2.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:68f5b7f7f755200f68c7181e3dfb28be9858162257690e539759c9f57721e388", size = 11749038, upload-time = "2026-07-08T01:35:50.071Z" }, + { url = "https://pypi.apple.com/packages/packages/61/36/67424748a4e65e97f0e05bf00df379dfb6c2d817f82cc3a4ce5c96d99beb/mypy-2.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:78d201accfafce3801d978f2b8dbbd473a9ce364cc0a0dfd9192fe47d977e129", size = 10704254, upload-time = "2026-07-08T01:37:17.971Z" }, + { url = "https://pypi.apple.com/packages/packages/28/cb/142c2097ca02c0d295b00625ff946808bdda65acda17d163c680d8a6a474/mypy-2.2.0-py3-none-any.whl", hash = "sha256:ecc138da861e932d1344214da4bae866b21900a9c2778824b51fe2fb47f5180e", size = 2726094, upload-time = "2026-07-08T01:34:00.075Z" }, ] [[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" } +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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" }, + { url = "https://pypi.apple.com/packages/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 = "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" } +name = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } 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" }, + { url = "https://pypi.apple.com/packages/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "pathspec" -version = "1.0.4" -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" } +version = "1.1.1" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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]] -name = "platformdirs" -version = "4.9.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, + { url = "https://pypi.apple.com/packages/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" }, +name = "pprintpp" +version = "0.4.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/06/1a/7737e7a0774da3c3824d654993cf57adc915cb04660212f03406334d8c0b/pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403", size = 17995, upload-time = "2018-07-01T01:42:34.87Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/4e/d1/e4ed95fdd3ef13b78630280d9e9e240aeb65cc7c544ec57106149c3942fb/pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d", size = 16952, upload-time = "2018-07-01T01:42:36.496Z" }, ] -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" } + +[[package]] +name = "prek" +version = "0.4.8" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/8e/46/e436a6eb9fdb4d3fd08d0ab7fdba19fe03a9e994ec810de57869b853bd8e/prek-0.4.8.tar.gz", hash = "sha256:d15d8bef72ab7b02c7dc01458ac9e05b3131534492b5ce9bb11c4f6f636fa868", size = 494570, upload-time = "2026-07-04T12:05:10.941Z" } 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://pypi.apple.com/packages/packages/5c/78/b4149c8913ced2e42debb49e261c4788a1ce431e84226921c2e1a7ea8545/prek-0.4.8-py3-none-linux_armv6l.whl", hash = "sha256:1f8f8cdc65836b571824c965daebb81b449f7e4a43894c58621f5708d5a185ed", size = 5668955, upload-time = "2026-07-04T12:04:41.588Z" }, + { url = "https://pypi.apple.com/packages/packages/76/5f/7f54a0087b6b2f1751aeb41266d9c15e66fd0055492814798ab818cd0414/prek-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bce1798e96d9e3a6e6abf435da7107e81452f69edb3ca7c6f90a457355ea46e2", size = 6030947, upload-time = "2026-07-04T12:04:43.8Z" }, + { url = "https://pypi.apple.com/packages/packages/6c/d6/f2829fc3902920c36b764a386fa303e71a8219dac25cb3827c575e84199a/prek-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab3a52db17254d701c3cebb7eea58c8230aa7c1959aacfd5b5f25de18edb15d1", size = 5572593, upload-time = "2026-07-04T12:04:45.763Z" }, + { url = "https://pypi.apple.com/packages/packages/74/8c/c5589955bcd5e3e33b67d8bc3110818cecac82a38fd6bc8b5dfdc5de421c/prek-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b3fcfd620523bbc3f51a21d7cd63449f659b9e2cf3582de12dd5949e23227b8f", size = 5847150, upload-time = "2026-07-04T12:04:47.419Z" }, + { url = "https://pypi.apple.com/packages/packages/2d/9d/1f2dc91bdb79d2c4714b27eac9477a51490fba5b4731330dbbebc76bd345/prek-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42e65bc8425e9d7f1691a13ca1da2e07807d1ba76c35740833354b945131689e", size = 5573738, upload-time = "2026-07-04T12:04:49.125Z" }, + { url = "https://pypi.apple.com/packages/packages/81/29/69a7b58e16ecbc5f3989bf4b028018d11a82dcdd320b93d6588d72f32aa7/prek-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f578492a8e0c9bc6b4bf6dfbba8716f647d4cd0769bf10ad6cf336e3096fd392", size = 5981054, upload-time = "2026-07-04T12:04:50.842Z" }, + { url = "https://pypi.apple.com/packages/packages/63/cc/9b9850a60c22ed18c7755ebd2d72c6eefb37fac58149d09f6adc4691c2cf/prek-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4335f9d5beb123a3884a7fe34f57c9f0828f4fbb7666beab4298833459b104f", size = 6751350, upload-time = "2026-07-04T12:04:52.529Z" }, + { url = "https://pypi.apple.com/packages/packages/01/e5/c425aa7272b430630119e6757def3a2007555ba8cbeb2630e0448e7a8b7f/prek-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18a8747df9c602e052881d3efb14dd7f7d62a59bd7277ae5171c9e7661d59d84", size = 6243881, upload-time = "2026-07-04T12:04:54.703Z" }, + { url = "https://pypi.apple.com/packages/packages/1c/da/accd3ad07fd2891d3c2777eb42435439fdf11982c51d60f087c0b6b6e102/prek-0.4.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4db639db481d5f854eff9b3d2108889e613b8c15868bcf6bdd777c7cee577436", size = 5848846, upload-time = "2026-07-04T12:04:56.402Z" }, + { url = "https://pypi.apple.com/packages/packages/15/00/3477704635249f21f5f98ce444cd7690c2aa9dc8d146a045db88ef2cd8c5/prek-0.4.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c3890a6f92316d2cf44eb50584e8d2b23a596dd70487022e61186a71a2ac0900", size = 5713942, upload-time = "2026-07-04T12:04:58.311Z" }, + { url = "https://pypi.apple.com/packages/packages/fb/e6/3ca4fabaebeadc976d9a92d1d9130674265355ea3b728418bad61583b097/prek-0.4.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:fc7e15c24c591a37c6ffce5b25a021b16c299ac2649f183d812b67d665cd6551", size = 5554725, upload-time = "2026-07-04T12:04:59.96Z" }, + { url = "https://pypi.apple.com/packages/packages/a5/46/2ab6aaaeff0cedb8955b2e4032071c8712382bdd423bb849718c3720180d/prek-0.4.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:36fe721704ff0c7624c1167639e23a5fe658bfd38c314f487219c9afd1eeb733", size = 5838595, upload-time = "2026-07-04T12:05:01.861Z" }, + { url = "https://pypi.apple.com/packages/packages/ae/8b/91398f2b6cd1629d5d8ca8c85b08eca500814a374313b0193f4aaf6ab6c4/prek-0.4.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:162e544abc394a8124f3a4ad68efee116bad09440e679dbd1675177335c2a432", size = 6357222, upload-time = "2026-07-04T12:05:03.845Z" }, + { url = "https://pypi.apple.com/packages/packages/b2/2a/ce5cbfaad36866134a21754640a05ecdba641fcd7ad15aa74cf3443f34f6/prek-0.4.8-py3-none-win32.whl", hash = "sha256:2602e46c8c5da7dfa69f60fcf88c2b57132ac623f49fb08bfb3094298c5f07e3", size = 5354388, upload-time = "2026-07-04T12:05:05.587Z" }, + { url = "https://pypi.apple.com/packages/packages/df/03/3bc908bc5f7e430315553e47dfa055f19923a3888f9afe4da19f244b5cbf/prek-0.4.8-py3-none-win_amd64.whl", hash = "sha256:7cb22da60bee41b89c4978c0bea7126a3c0ccc003dae6748cf29b53947815edc", size = 5748221, upload-time = "2026-07-04T12:05:07.559Z" }, + { url = "https://pypi.apple.com/packages/packages/dd/a7/4295e6d5f5028171dfeb115ad38ab76bf3fe0c8df91b70d73c79aa760a94/prek-0.4.8-py3-none-win_arm64.whl", hash = "sha256:da70057f577b15d4bd121bf9dd29ee205fd4b4d75a0cafba062e84d7e8b4378b", size = 5574425, upload-time = "2026-07-04T12:05:09.595Z" }, ] [[package]] name = "py" version = "1.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload-time = "2021-11-04T17:17:01.377Z" } +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload-time = "2021-11-04T17:17:01.377Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, + { url = "https://pypi.apple.com/packages/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, ] [[package]] -name = "python-discovery" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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]] +name = "pyjson5" +version = "2.0.1" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/f1/9a/3db19560e968d6e85b2a4ddf4b949c6ebf9dd1dcfb5a9f37736f8adeb927/pyjson5-2.0.1.tar.gz", hash = "sha256:a5b0e322e847b198a50d8a1ef16d6b2b19129644dc018d76773e81ef1487ca39", size = 352242, upload-time = "2026-05-15T16:12:54.931Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/68/41/be622b742fe4749fefa9d8a923b1cf5e51a1e8e4b4930fa8ea8c531b243d/pyjson5-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62394660d54e76ff8a968c8194b252be23d184f05a00238d4def9624454ea6", size = 302636, upload-time = "2026-05-15T16:06:08.006Z" }, + { url = "https://pypi.apple.com/packages/packages/4b/e2/50378c3bec9164c1bb983ca15ed59f8be498981b790a90d1410a1016ffe2/pyjson5-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:58dfa4cf5c327e14c530b16c23e812c021fe53b19e6f9cdfce257d8c02244895", size = 158588, upload-time = "2026-05-15T16:06:09.684Z" }, + { url = "https://pypi.apple.com/packages/packages/cb/7d/5de0f82c144b6fc2fa2e3fde5517257733a91d24734b5d2415b7e991e8fd/pyjson5-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:07cb90a31f962a6de9e49592a306046ee015da73510c6d215e9481a2a21c9ac2", size = 153741, upload-time = "2026-05-15T16:06:11.114Z" }, + { url = "https://pypi.apple.com/packages/packages/f5/ac/174ca02e5f3ed8245dc48247e534880bbec1528c0c4eaa768fafa9417dda/pyjson5-2.0.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ef4026a90a68cd2bcb64d89ca064a1615b60a61c7694714e2b8ef30434f365b5", size = 185450, upload-time = "2026-05-15T16:06:12.883Z" }, + { url = "https://pypi.apple.com/packages/packages/8f/0d/bfc553176b7e109b72f23697ab1fd1a77d7fd5c6626d6e23936f13d5884e/pyjson5-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04b4929a7adf0e0720da92ac54c92d408e8057158be3ccc34da17c7767a9af78", size = 163331, upload-time = "2026-05-15T16:06:14.314Z" }, + { url = "https://pypi.apple.com/packages/packages/b3/05/b0e67106f78c8ea5ffcf6ce0b8421f38a04d5f37ea88ac70123199755bcb/pyjson5-2.0.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e889b039d6bffda9a6b8d822073bbdb4a60166af22f075e7faa0c03c2dfcf17b", size = 166429, upload-time = "2026-05-15T16:06:16.076Z" }, + { url = "https://pypi.apple.com/packages/packages/81/3c/2366f8ecf053434470eafe1f120f2e235d13015fc50d7f06e55e39b550ab/pyjson5-2.0.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a08e99d0a33463728fb016f5eec680504499c4be280dac016de2da70ec55b0db", size = 180499, upload-time = "2026-05-15T16:06:17.544Z" }, + { url = "https://pypi.apple.com/packages/packages/76/65/9a70876d4dd09ce17678024266406b94b8f1edf9db6c2e3bf76cfa653a13/pyjson5-2.0.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:035c094feb6a9d4312183f9f34228d976102eefb971e9e3dd972d8c7900a466a", size = 187469, upload-time = "2026-05-15T16:06:19.212Z" }, + { url = "https://pypi.apple.com/packages/packages/5b/43/0c0bcbf9a9ef7fdcd49dc0992a3a206244c2c2baa7b409932464cf26f1a5/pyjson5-2.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d178025a5317db44663dc87aab90f34674991b3da40dbedc3c108e7b03b6466", size = 176032, upload-time = "2026-05-15T16:06:21.153Z" }, + { url = "https://pypi.apple.com/packages/packages/82/2c/32213674010c44db265d45e977a431cbde971c45342b483f7af337811403/pyjson5-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:381ca5b3172bccf9c29c5823a0173ab9118d579d0f5bb4fca20b803daa6e72d5", size = 171477, upload-time = "2026-05-15T16:06:23.017Z" }, + { url = "https://pypi.apple.com/packages/packages/f1/49/da5417d592ac75a23d1ca2dfda846391ccf137edd89054b780866c592b65/pyjson5-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e158caf0dd7fbf7afe12f92fcc9cba26bceac4b38615c087fd592e9e3240d9f", size = 1145571, upload-time = "2026-05-15T16:06:24.757Z" }, + { url = "https://pypi.apple.com/packages/packages/60/f8/1f0c326b3a3267f28b7b9e2ac07121386ec20ee8f9dd49955af4faa17ba7/pyjson5-2.0.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c8010e9a02b6a0719234f8d074639f9051afe579ff0f74d3367af1d8b4c7d839", size = 1010388, upload-time = "2026-05-15T16:06:26.574Z" }, + { url = "https://pypi.apple.com/packages/packages/de/c8/76f2739a8715061755f12f4d2795f6c4079aeadd166c56d0992521442d5b/pyjson5-2.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3543e067d1d8ab6cb1cd41add38a42eaf82e9a7e802e8a08f1acda9f22199b00", size = 1323111, upload-time = "2026-05-15T16:06:28.805Z" }, + { url = "https://pypi.apple.com/packages/packages/50/7a/683e1586fa076fb344aec23a647d67f427847b6bffb1001d56e86aa8da33/pyjson5-2.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3e496938e7e0defc33bfbd0ff581d4ffdc428c598cc5d679232f34df27f7330a", size = 1240671, upload-time = "2026-05-15T16:06:30.473Z" }, + { url = "https://pypi.apple.com/packages/packages/c0/8e/6638b0b22344b23cae1271b0211668be54b3f76183bd8d4f89a2edd8e562/pyjson5-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2badbc101908123905a38789cc552b99ab896a7ec0ce4fa6ee2bb7b613c85c1b", size = 1178013, upload-time = "2026-05-15T16:06:32.212Z" }, + { url = "https://pypi.apple.com/packages/packages/7a/21/d44540d3cdc670e5c6e665471361f557ef8c7d56964cd433524208b3707b/pyjson5-2.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:b1b0df4898e2e046fa1e9b5df436fe11d9b681b1e5fc9877508d982d2059aaf1", size = 1354907, upload-time = "2026-05-15T16:06:33.922Z" }, + { url = "https://pypi.apple.com/packages/packages/3c/74/fceedd2709760f5c85e75b508002af9b4c2da8af20a80e62b2d0e2358a15/pyjson5-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8fcc96c429888f2293f672e33d01da8a0847cd5116a773c9a1903b8bc2f12bc1", size = 1208847, upload-time = "2026-05-15T16:06:35.883Z" }, + { url = "https://pypi.apple.com/packages/packages/b8/3a/d2f062b5801b3034ecb16ffb0801d7b4a1d6abae4cd4615482dbf62f1ed8/pyjson5-2.0.1-cp312-cp312-win32.whl", hash = "sha256:48c97e2f7f02171948f8a7ed6c9b2b2faea00653a3348d9e810238a688f07b6c", size = 115562, upload-time = "2026-05-15T16:09:24.807Z" }, + { url = "https://pypi.apple.com/packages/packages/26/13/2265cf16720defdc87672b68ff5edf83820bd76e59e32f8384c9cb1ae619/pyjson5-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8dea9976eea0aa7af8b5ad49df3fa831d6bda08ffd7bb11409d6a81961491219", size = 136176, upload-time = "2026-05-15T16:09:25.931Z" }, + { url = "https://pypi.apple.com/packages/packages/9c/92/a8947e646ce642555ac22b31f44b7168c81b0e364d0f2711d571955229b6/pyjson5-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:264ab65fb7d68a763567c2302151f1f074bf053c8479939aa8e75f5f9518fe31", size = 116752, upload-time = "2026-05-15T16:09:27.274Z" }, + { url = "https://pypi.apple.com/packages/packages/93/2c/0619f89a9576f335ba63eb851e73ba507480a8c7f28a2e7de2501ed3d303/pyjson5-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8a661d292801b38434d5288bf7329f9b50b3a693b1d2941c7595175842692eba", size = 301821, upload-time = "2026-05-15T16:09:28.721Z" }, + { url = "https://pypi.apple.com/packages/packages/91/31/7824913ec71e7421d6a57bc06228f3e2d946d8e8f738f898572dded0dc57/pyjson5-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cb5c1c066038ce6e1922d9d5be23f5cd14d5b5a3c96e6358aa5d0e379014a93", size = 158201, upload-time = "2026-05-15T16:09:30.261Z" }, + { url = "https://pypi.apple.com/packages/packages/bc/7d/4ddb249563a838425242342d2cf67976ccc292a831b543a92fa1a8a83b11/pyjson5-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7417eab751817fa5f070e41975d9df4aec3532d21389073318161f63cd96d636", size = 153244, upload-time = "2026-05-15T16:09:32.04Z" }, + { url = "https://pypi.apple.com/packages/packages/02/39/7622416ac0570d9ce377447bd5b2ec9383c1282e63cc6d0b65779f1336fa/pyjson5-2.0.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cc4802093b4ab9039367774988486fd04754cf416c32aebf94a94bafd8b8a478", size = 185568, upload-time = "2026-05-15T16:09:33.719Z" }, + { url = "https://pypi.apple.com/packages/packages/d8/90/2f317da231477b77481020d79c73bbe10625e4925ceeae77603726ef1763/pyjson5-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc3181274ed19ecb2315cf85a499bf83002554f42c72453666c616059d665a7b", size = 163169, upload-time = "2026-05-15T16:09:34.865Z" }, + { url = "https://pypi.apple.com/packages/packages/02/b3/20023c3cfe2f2c2523007d7031b5b7ad7ccd8856d2665547683cebaa6f8e/pyjson5-2.0.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5398306b8aa253e620af9ed8085767de9cd28d6846da45e535e80fcffa9f33d7", size = 166514, upload-time = "2026-05-15T16:09:36.383Z" }, + { url = "https://pypi.apple.com/packages/packages/e2/1c/b863e502153477b2845a54b688b72436457aca5107b300fca95e98184551/pyjson5-2.0.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b97b592287d52a6ec5af84dca94aab02b185f72f8ddb6fc99c58efb8891be5cd", size = 179907, upload-time = "2026-05-15T16:09:38.166Z" }, + { url = "https://pypi.apple.com/packages/packages/fc/b2/413fcb76632e5f6fa89a2ec83976755b5bb4696c6d9eca9ff3c70cd717b6/pyjson5-2.0.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4efbf5e910a3aa6f330a92d1ae940dda5ed662976ed5056526baaf2e1821f95d", size = 187171, upload-time = "2026-05-15T16:09:39.624Z" }, + { url = "https://pypi.apple.com/packages/packages/d6/11/66151b819407ab589aef36582038257f1ef42dc065e3423b3d8274f4fcd2/pyjson5-2.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e042f9b869e7f21d5f53a2638e5d0cf1daaba2342127c8777c502daf972602e", size = 175564, upload-time = "2026-05-15T16:09:41.356Z" }, + { url = "https://pypi.apple.com/packages/packages/f2/98/deb70690dab994474ea527cba91f43ae55928bcff498c441e812b60fc1e2/pyjson5-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:26074370d7a6dd38b6e8c28a2f10c790fc6dec938ec22ec3de6c93c97d195f10", size = 171530, upload-time = "2026-05-15T16:09:42.78Z" }, + { url = "https://pypi.apple.com/packages/packages/a9/be/969752a3a052d00698b6e1dd926c627d7a6475a7c8dd390db148363544f8/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3cb163a495096716a7a01d052b60bda0308ecfaeddcb2d50a247ba9d707e3e40", size = 1145510, upload-time = "2026-05-15T16:09:45.005Z" }, + { url = "https://pypi.apple.com/packages/packages/d0/be/4c5c92cdda5a911ee4450a74281782335e7ce6715638308322beb96be639/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5fdd66bdddc2d53421ab7427bedcd2ca9b2f1b4b3b464381aa60d305c94be38c", size = 1010114, upload-time = "2026-05-15T16:09:47.081Z" }, + { url = "https://pypi.apple.com/packages/packages/89/1e/72283bc505d77dcdab7eaeb0020cf0fd79a05d2991e886d1fcfa73e88ae1/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4ad222a3eff1cc93f9b70894c5f3d2ff5e46531e88feaa4818e329d5f3ae4307", size = 1322915, upload-time = "2026-05-15T16:09:48.885Z" }, + { url = "https://pypi.apple.com/packages/packages/06/b4/94a09e744a6bb6e76108b61a28a7cc5ecab1b8105cacde7548359b3b74a3/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:63f5cf25113dd1bf3bfeda211d99413e58ec6b9036ff34f7b69f85f23695e9c4", size = 1240126, upload-time = "2026-05-15T16:09:50.638Z" }, + { url = "https://pypi.apple.com/packages/packages/af/29/4549380cae425ee112f3c154606c35f5211c12ddf415f079c2b23f2d493b/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b0001eecce9080e6c170e978786501e8931e2075a3d15f5260e1bdd6a45e8976", size = 1178022, upload-time = "2026-05-15T16:09:52.639Z" }, + { url = "https://pypi.apple.com/packages/packages/1c/1a/5a8a869e855645858e45dbd077204cec3a85b2f2e669bc3c8cefe9c4a70e/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40371c73cc83ed81914028786a6cc5950bdf300ae82443df83a5dfa80c582fca", size = 1355161, upload-time = "2026-05-15T16:09:55.177Z" }, + { url = "https://pypi.apple.com/packages/packages/82/32/827066cd946447648275a892f494f4572d78e6a79d877fa6b6c14af599d8/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c9dca542eeb6e8fbbc1fe3373bfd18ae6250f1aedc78f39242ac12c8837091b8", size = 1208864, upload-time = "2026-05-15T16:09:56.919Z" }, + { url = "https://pypi.apple.com/packages/packages/2d/cc/a9bc12aff47d8bfd3074a21e3fb056ec0028fac28f8ec7e0fe4449d050f5/pyjson5-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d30c8b2c91d530be475a8fab6dde8c3bcd3e89b999c9e0f05aec2bf2c24b0638", size = 115461, upload-time = "2026-05-15T16:10:30.711Z" }, + { url = "https://pypi.apple.com/packages/packages/c9/c7/fa3fc956fbc3fa6250c8b99ef75a8f52b691780fca1bfb368340283bd898/pyjson5-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:67f8f5b8d3e3b2ca5f618c928729986aa00fb588c476c0d0d3a151633ff41e0d", size = 135836, upload-time = "2026-05-15T16:10:32.062Z" }, + { url = "https://pypi.apple.com/packages/packages/f0/0a/e457b20d1e36a766d3ccd09c418b7ef41acabb679ff2248bb1cd38247ce6/pyjson5-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:bdf30ce5b1242f63254d936b30af75cae237fab0ee71cc2544163c82b6bc9201", size = 116652, upload-time = "2026-05-15T16:10:33.39Z" }, + { url = "https://pypi.apple.com/packages/packages/2b/4c/6266789e615576b62d2db6de26a3b4b8f4cc7a8ff23fe24e48363bca1682/pyjson5-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:954c14d40022fa40e0d36e0ebd337e9e90fd71145592d25a0e2868344e3daca0", size = 320487, upload-time = "2026-05-15T16:09:58.38Z" }, + { url = "https://pypi.apple.com/packages/packages/dc/75/c4a563034805e20b274fe4db963e1d480001931d88fe70b7d082033b7dcb/pyjson5-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f57529b98d21e8b76f8bc51af9d3c3057dd04a92287fd21ef7ec55255ca6f5c5", size = 166879, upload-time = "2026-05-15T16:09:59.849Z" }, + { url = "https://pypi.apple.com/packages/packages/f3/d2/5e6ec3580379794e9a99a402465f74586e93e93814974172202d1254fa89/pyjson5-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:545b655ef0b59f39fc29e2b63b4dadd45e447ad77bad2fdca859ff4db69e21f5", size = 162235, upload-time = "2026-05-15T16:10:01.147Z" }, + { url = "https://pypi.apple.com/packages/packages/5b/48/b2d0e868ef8375eb0696ddc74f71028fc4fcd26cdddaeb34485214f2dc87/pyjson5-2.0.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b519de49dd4cbf254179749e5c6bfe9bd8fd1f97e7755bff2e17cc6f79f85aa5", size = 182162, upload-time = "2026-05-15T16:10:02.523Z" }, + { url = "https://pypi.apple.com/packages/packages/ce/18/fed02a3d68c3badb87394e8420b72d7cb165ade208c1e02561637409aa99/pyjson5-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de48fbd0466c114c17f408be7bcbc5b3aedeb29ff5f73187022915e6d5eb7817", size = 169487, upload-time = "2026-05-15T16:10:03.876Z" }, + { url = "https://pypi.apple.com/packages/packages/8b/90/dc897332dda24e949828616e8e74d7937b587a56789f3aca148fcadddde2/pyjson5-2.0.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fe6516ff9ab94368c18ff8ac0ebdb53fac9e047da093d43e67d5e7cd98cdd0bf", size = 164128, upload-time = "2026-05-15T16:10:05.588Z" }, + { url = "https://pypi.apple.com/packages/packages/39/09/90104baeab58adfe63bda804174ef1806b1cabb1692e047fabc2f4aa1799/pyjson5-2.0.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ea7aadacac7ac6661117b708175f6beff75e132322c08bbf3794d8ec4ba572b4", size = 184306, upload-time = "2026-05-15T16:10:06.986Z" }, + { url = "https://pypi.apple.com/packages/packages/89/73/613efc3cdab5cf9ec399e6c5a0463b672e5f5cbd693a3a4d82cbdda19476/pyjson5-2.0.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5c339bcac113dfc30a1f88f23af273d67bb6169abee1684c0dd27daa71a32f9", size = 192239, upload-time = "2026-05-15T16:10:08.727Z" }, + { url = "https://pypi.apple.com/packages/packages/47/2a/17b97e02b37ab9353fc9713e56470be3d2539501ed18d6c676cfbcd0a57c/pyjson5-2.0.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:075de0f1e5e0ffee174a8941b5d9d8af632772a6e5eebe92fb0899db7f6ab7fa", size = 177652, upload-time = "2026-05-15T16:10:10.19Z" }, + { url = "https://pypi.apple.com/packages/packages/1c/43/a05331ba88fd1aa09ca418548c6b06692e34b2a64b109bb363e01ecafdeb/pyjson5-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c463c9bf508c1d316191898a3c0bfc03dd3de67bdc02f480b0742ac5c859eaeb", size = 174860, upload-time = "2026-05-15T16:10:11.976Z" }, + { url = "https://pypi.apple.com/packages/packages/2e/00/5d253751f4d27b7e63fdfd765041593fa588395f45b9cea496878d964448/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:54427c8cad0c1a516bad5b4457167cde1e7ef7ee563e28a944f59b3c7413b6a0", size = 1151556, upload-time = "2026-05-15T16:10:13.608Z" }, + { url = "https://pypi.apple.com/packages/packages/9c/f7/a30eed477295e92dfd59553063cf82bf510a7052efc7720c9c50d0082fdb/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ca04839cecd364cb53de7ebcd81fd5915193707d968730ea1c8d4ff5c9c2f1a8", size = 1012330, upload-time = "2026-05-15T16:10:15.171Z" }, + { url = "https://pypi.apple.com/packages/packages/9c/60/80818fad90a99336f8bfc686d76d0b2d7c8bb51e8de5d031620fa1a2d7a1/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13a2de8b14aeb576b1623c8af419510bf871b8d9cc2308db41a46bcc94d00f2f", size = 1320970, upload-time = "2026-05-15T16:10:16.863Z" }, + { url = "https://pypi.apple.com/packages/packages/7c/3f/090eb3e0971d067defb58bbfd738f74a09be495c6f4f00d5d76a10755bca/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c8a689f5252e9791bfbc8fa916999202556a1e8c7b24dfd72de1ad84c493e10a", size = 1244775, upload-time = "2026-05-15T16:10:19.416Z" }, + { url = "https://pypi.apple.com/packages/packages/61/38/e7546ad733affe51a5462bad21bceb4fd659baf24930bd141627ce56279b/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3dc0c2c80fd9d1e4c26f8b6c91a54d38b48648b93ae3f09355856c75fda3e460", size = 1182149, upload-time = "2026-05-15T16:10:21.696Z" }, + { url = "https://pypi.apple.com/packages/packages/90/2c/016647580d8a82ee53b4cb9ddb96deb4b157ccea9624bba61de4aa2cd25c/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:13d78b3e2c60b81bd7e6b00e6ab24a664ba1fd64b5d86baa65c4cdfae1fff7ae", size = 1358579, upload-time = "2026-05-15T16:10:23.649Z" }, + { url = "https://pypi.apple.com/packages/packages/01/45/8b84ff7a0c4d8c12a76e097c67af62abe986a18d1ab2540764dd6eaa0253/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a7b640dbeabbd7d975170f793f90a7500cc5d510aeae9364a1bc558bcc5336ed", size = 1211015, upload-time = "2026-05-15T16:10:25.4Z" }, + { url = "https://pypi.apple.com/packages/packages/cf/48/d8f34de7a7319f5966bdfd10f133e689ba01f138d2040bf792f1ca8b18e1/pyjson5-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:46e7c5525034fde8abf3aaf100ed7660c1b618ccd3205544ed7c99e4c55cf201", size = 131660, upload-time = "2026-05-15T16:10:26.747Z" }, + { url = "https://pypi.apple.com/packages/packages/06/aa/0bd437252134115a846ffc061078c85f8e8c325c86abcaa862c134d3c57e/pyjson5-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:207c00e7f9641e0358bdc848a1c0610f26f2e8bd51d73bd64e5a0ab3b725f1d8", size = 157717, upload-time = "2026-05-15T16:10:28.053Z" }, + { url = "https://pypi.apple.com/packages/packages/24/61/7849b04a0dc78f73905189400ac99c45de8eec2ec451ce9c6990fb3588e5/pyjson5-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4917d5b6186bfa48ec4d85326e2a09769a5f7844963307ab2710429e1f743264", size = 126281, upload-time = "2026-05-15T16:10:29.378Z" }, + { url = "https://pypi.apple.com/packages/packages/89/41/70aa1cb1fb0a3ac4c9b8cd405c0c85ba935c53fdfae6271b8eae364b92d1/pyjson5-2.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:573fecd7cad4e24d232053f9ebf331c80fe1b0ed6e2064f7614797a7f691e7ca", size = 303706, upload-time = "2026-05-15T16:10:35.163Z" }, + { url = "https://pypi.apple.com/packages/packages/0c/d9/caf44bf3d33b9502dc4b8ed5d0c7a8af8fbe33001e82414c0407f39f18bd/pyjson5-2.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a8ade508a046a5407a322c098e3b7e6033216b158a7746eb8962b7a4cdbf9248", size = 158915, upload-time = "2026-05-15T16:10:36.496Z" }, + { url = "https://pypi.apple.com/packages/packages/3f/94/f2bab1ce2eac8f77e16dcd0a1ba39de20733a84b9961c3504af0dd68bceb/pyjson5-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4635e349bb1d1f1e854dc790f14be31b6cb198d6453848e8d86818104074f805", size = 154199, upload-time = "2026-05-15T16:10:37.902Z" }, + { url = "https://pypi.apple.com/packages/packages/82/c4/d94573ca37486af3d6a72f2582844d7d490d8e55639e0cfba371ce91442f/pyjson5-2.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d274697f81f143abac12ce256a06b61635c30d0f8cc11eefae7182655dac9a5", size = 186060, upload-time = "2026-05-15T16:10:39.603Z" }, + { url = "https://pypi.apple.com/packages/packages/03/cc/c42e697def319b286fdcb912939c044cc94bd1cfc7338b6dbb566f817f29/pyjson5-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be4e2242e55a2651fd8696cfd67ec8e04f54f4ed6c089cceea2b63763a7516d8", size = 165796, upload-time = "2026-05-15T16:10:40.967Z" }, + { url = "https://pypi.apple.com/packages/packages/3f/dd/f22a5f0e619ef22b8e32520a91bd92f815d50f9ec2d67cfe5974eab9476c/pyjson5-2.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af855c680feaa39cae4a44914ee2863eeae1549f37ce58069c747b3d4803999a", size = 165262, upload-time = "2026-05-15T16:10:42.391Z" }, + { url = "https://pypi.apple.com/packages/packages/99/8b/90e22ecb12d51ccdc68325b6b051002e6103cc4600a335d9ed13828acdd9/pyjson5-2.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb769d90516da904e6cf0ba58f3d5830f4a5804486b5e85c5ca42ba3146d187a", size = 181608, upload-time = "2026-05-15T16:10:43.914Z" }, + { url = "https://pypi.apple.com/packages/packages/e0/ab/237f9036eed73e08cf8861466d3d1182ef1c88506bd29955740dcadb24ff/pyjson5-2.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6484c14e07aa46abeb3cb2ff0204a765260763ad7cf3f87f601f26b20ed607f3", size = 189582, upload-time = "2026-05-15T16:10:45.367Z" }, + { url = "https://pypi.apple.com/packages/packages/c4/3d/3df8d5f003910a9291e5f04fa178f626e3c8553a5986dc62589ca74195ff/pyjson5-2.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc03673adb544324500d79b2acfe2ced038998b2aceb564d335de9cc238cbdb0", size = 176321, upload-time = "2026-05-15T16:10:47.134Z" }, + { url = "https://pypi.apple.com/packages/packages/c0/d9/a458a54f780bafec1b44ee3c27cb3007a83fb7bd4f1c17ac6bf519fd6151/pyjson5-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d714daf784bec2e14fc06c0c26e4a373a6157eaeddaed2d5a7b7b6ae2aabc33", size = 171956, upload-time = "2026-05-15T16:10:49.174Z" }, + { url = "https://pypi.apple.com/packages/packages/78/d8/1010e0147c8862dc0881cf3656364d3d87a5e336d290fe4cb0b92223e14c/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5cf3ded356730e08b5941a16db6525efc0de26f35468ab09ad573056c4efc6e5", size = 1147679, upload-time = "2026-05-15T16:10:51.005Z" }, + { url = "https://pypi.apple.com/packages/packages/19/2f/91c8d8cdb4a2e4bbe8f14b9b57d717988e14830f2ed4a4f08d503cb5edde/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cc00d4669fb4170c28b2c9ed3cd1c2af9f21727c55d6866030cc154f31f68747", size = 1008216, upload-time = "2026-05-15T16:10:53.252Z" }, + { url = "https://pypi.apple.com/packages/packages/d7/d7/25c3f8693660a35be7bb4eb71b0d10d9a6981e986723a4881d607f0c6462/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a77a91c2e019476345c03cde105da44100daaabc3fa56f82f2bd9eb2ebbcb698", size = 1323856, upload-time = "2026-05-15T16:10:55.517Z" }, + { url = "https://pypi.apple.com/packages/packages/c8/25/82f0f83556ff68ec0ca1129ba9afeb14149cd421dc74823cdf10c209c95c/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9ae54e5d717982cb7a082c700ab1f7f965c6aedea6fcb003ec4bdeac4f02bf52", size = 1241860, upload-time = "2026-05-15T16:10:57.635Z" }, + { url = "https://pypi.apple.com/packages/packages/c1/01/2c4695fc06a0f3f29041a875d3bb24f904c9ae7b3d8dbfa1f8db17ce995c/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ce4f5b67b3a6fab623a31d83416b51a5b2c97cb172194f214078484bbb4783a6", size = 1178443, upload-time = "2026-05-15T16:10:59.398Z" }, + { url = "https://pypi.apple.com/packages/packages/9a/ae/c10f534037b096aca21b90017ac9d41f791572dc3ec31fcd92db8ddcd256/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f8d2e61f7bed0b40cfe5f62375581bf56d7b5b9707c9d74747b5931149d7d376", size = 1357739, upload-time = "2026-05-15T16:11:01.189Z" }, + { url = "https://pypi.apple.com/packages/packages/0c/f0/655224f78ede087aa6a295c4745423e0104dd31f67928675b1a4cea72a0a/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2930140edd1a64eb42689993c5ae317bca2e6bc785b5dc5053bd5e9d184c98ad", size = 1209542, upload-time = "2026-05-15T16:11:03.364Z" }, + { url = "https://pypi.apple.com/packages/packages/73/52/a7b2a26625136fcd0f92e17beaaae51e04923cc2e28502db354d4de061b4/pyjson5-2.0.1-cp314-cp314-win32.whl", hash = "sha256:51733f91dda897239ca10928f363ce6f4b5eadaf797e74477f6c1c3222c185a7", size = 118342, upload-time = "2026-05-15T16:11:40.009Z" }, + { url = "https://pypi.apple.com/packages/packages/3a/8e/79db426fff3a41610076989d4060cc18a6508ebd3c7877155f6709eea102/pyjson5-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:698c73aacff49ea35bbbf7f700a97785626201ea7aa2e1f1e0a4fe23788c3be2", size = 138408, upload-time = "2026-05-15T16:11:41.757Z" }, + { url = "https://pypi.apple.com/packages/packages/c7/1b/385da14c05412bca15e86551dde91959686c8bddf7e78722bdc627fa6815/pyjson5-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:d447e2e5756f89abfd0ad82d1438075bcbc701c1cc9279b43d24825aaac67356", size = 120905, upload-time = "2026-05-15T16:11:43.146Z" }, + { url = "https://pypi.apple.com/packages/packages/dd/04/cd69739556d304d3ea48064ea7d47e5ff7321b0032d7ad78864bceaa0cae/pyjson5-2.0.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5603be4f0cb9685d7c2cbd408ddec21b33f415252bee00ac7969f20b5789a1d1", size = 321150, upload-time = "2026-05-15T16:11:05.07Z" }, + { url = "https://pypi.apple.com/packages/packages/84/d4/6d98268ed07a2cac1e79634fcd3dc280d2503ade5f3c465f7aa095951444/pyjson5-2.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:87267520a256b3f2dba7f2995b745e8b3f2dd72bbf8436010a5191f7809f8ab6", size = 167540, upload-time = "2026-05-15T16:11:06.822Z" }, + { url = "https://pypi.apple.com/packages/packages/c8/c6/8855a9462bdbc8306cba62793e23cc2952dfcbcbcab8e0413f570c891361/pyjson5-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:77e58307b74019d90aff9f2781bcffbbea3315b15fc814f8fa9b8d334b956ff4", size = 162533, upload-time = "2026-05-15T16:11:08.218Z" }, + { url = "https://pypi.apple.com/packages/packages/a6/a3/1092f68538ee71ed8027393ab5e71aa9c4a114e493e30a55e46e34c2ffb9/pyjson5-2.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd1712038837342dea73ed526883e53c6e85a158242b6f574b9ad9cdd3199c7f", size = 184165, upload-time = "2026-05-15T16:11:09.639Z" }, + { url = "https://pypi.apple.com/packages/packages/46/63/bb9f42a4047284ea59064c134c88dfac3b7f5273e40fe548d6a086cea452/pyjson5-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dba36677c77aed5d680827c65478933705ca156a34838f1250ea191ffc27662", size = 170075, upload-time = "2026-05-15T16:11:11.105Z" }, + { url = "https://pypi.apple.com/packages/packages/55/2a/0379aa5184e986d0f9af2919712c148d81928ce7d2d7950407e271b1df3b/pyjson5-2.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:04356d0c09e58907f60675d33859f269473479b90add5026231ef2fadba5207e", size = 164100, upload-time = "2026-05-15T16:11:12.909Z" }, + { url = "https://pypi.apple.com/packages/packages/15/d5/2087af69695c28c7fe796afc73dec7f0b0b9bc123ec329e45928d58d4705/pyjson5-2.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b331a0fbe2ae26f4ccb88045ebaecc00aeeeab23ff858f9a2223cedd969ec6d6", size = 184674, upload-time = "2026-05-15T16:11:14.448Z" }, + { url = "https://pypi.apple.com/packages/packages/2a/06/cf1b2744e07f1689cb0ac663e162a060b81cd358474754fe9f3bf02d5398/pyjson5-2.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ca55878d923ba5764254ad1c020c22b33e6161b3555d1f457b8060f2b3c6c17", size = 193047, upload-time = "2026-05-15T16:11:15.969Z" }, + { url = "https://pypi.apple.com/packages/packages/54/e6/a6b0deb6a3f393907c6d9115a7ac8e27557109204b56b05d3b57c51fc2ca/pyjson5-2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35e18d11e4c2034b78a664affe18c153a8efbf8872cf6bed7f12dc4fb819d440", size = 178712, upload-time = "2026-05-15T16:11:17.364Z" }, + { url = "https://pypi.apple.com/packages/packages/6e/48/9e5c16daba56dfdb481baab85d1197e14ae3f403902fc23cdca14d14351b/pyjson5-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b710a7489b6e134890eaa1fdb285e9644a1e730c1cfcf1cd90a87859b87a84a9", size = 175642, upload-time = "2026-05-15T16:11:18.781Z" }, + { url = "https://pypi.apple.com/packages/packages/c3/32/f07d4dbd8cf733f9e3f3e9c9abedfbc23da1e5cfa7fed46aebae07ca6f25/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:08920c0dd6aba3fb6bc6c849e6f05731c5fb2716cd651de424ff60cdd12eae65", size = 1152219, upload-time = "2026-05-15T16:11:20.54Z" }, + { url = "https://pypi.apple.com/packages/packages/01/9d/0ddbb89d381bc27f3740850d5d664c1f6f76620ad879ff5eb2506a4dfd6c/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e166dadb3275025cff5ee8602131372a3ea171c6727b3fc6e44697348e3972c5", size = 1013305, upload-time = "2026-05-15T16:11:22.702Z" }, + { url = "https://pypi.apple.com/packages/packages/bb/93/0f44886391dd2249ad9cf98c2deb3f26d1a115dd5a215629af7455765968/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ea3c2b4e7b8e209e7f59bd6a925d79b69cd0a4ffa12f2df0c6af55514c8b2227", size = 1322840, upload-time = "2026-05-15T16:11:24.918Z" }, + { url = "https://pypi.apple.com/packages/packages/d4/29/c478dc24dfcfc07e407ff8669df265ecc51fed5e7d47e51a01f17fd1c53f/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b710fc8ce00c984c8865131f05a8536163e1d3caffb9d081647879eca7424670", size = 1245221, upload-time = "2026-05-15T16:11:26.873Z" }, + { url = "https://pypi.apple.com/packages/packages/e2/f0/5fef8c47c5b7052da79af6425a021040402a9a09b37c5687d91f98696a27/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9784d79275d1b06d95f2920c3f260ce5d3f6671f65a1fae1464f83fbfc834a0", size = 1182991, upload-time = "2026-05-15T16:11:29.258Z" }, + { url = "https://pypi.apple.com/packages/packages/90/33/4d3a9d3159cdff1f1886d39f1991e6c8d5927f9b606cfc9743b5bef98c2a/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0ef86c53d0e14991b5b0fe58f225e3d8faec437aa0a70da4762525b6bc2fca10", size = 1359104, upload-time = "2026-05-15T16:11:31.651Z" }, + { url = "https://pypi.apple.com/packages/packages/0f/04/20eb16c52453aea0af68a697f4058378c9ff871bd2f0bea28eb76ffe12f8/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1d9faccf5a9e86f14104eec31c13428ef8baeb867182ce107e0c68fcc5c62477", size = 1212847, upload-time = "2026-05-15T16:11:33.767Z" }, + { url = "https://pypi.apple.com/packages/packages/b9/a3/69bbe93275eafb7802a5b29230cc5c28f9b924c68d15b3ab2d43cfe19825/pyjson5-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:2df497af7ab03cf82d9278a4707a83d0ae4e6d727f919a883b5ccec3f7d92650", size = 139137, upload-time = "2026-05-15T16:11:35.487Z" }, + { url = "https://pypi.apple.com/packages/packages/48/45/b865ca6e0ae6887bc2b6b17cd123470ae43ac0fa04a2362d77d4b9f5bd43/pyjson5-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:3b31cf4f4a4f01800812865af3b02f6700cefb9377e4d9a9c6b78fdcf41fd973", size = 169412, upload-time = "2026-05-15T16:11:36.887Z" }, + { url = "https://pypi.apple.com/packages/packages/68/55/02c734459fef0a955ab3e7e745df415d5f9fc873a4c288765f60f22fbc13/pyjson5-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:15f0d8baea89d35c6c01c60b944c778a79cac10f77f71b727f1b244e2c7a8ccb", size = 129061, upload-time = "2026-05-15T16:11:38.712Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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]] +name = "pytest-clarity" +version = "1.0.1" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "pprintpp" }, + { name = "pytest" }, + { name = "rich" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/52/5c/cafa97944de55738a6a2c5a7cee00d073cb80495032d2b112c4546525eca/pytest-clarity-1.0.1.tar.gz", hash = "sha256:505fe345fad4fe11c6a4187fe683f2c7c52c077caa1e135f3e483fe112db7772", size = 4891, upload-time = "2021-06-11T18:16:18.372Z" } + +[[package]] +name = "pytest-html" +version = "4.2.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pytest" }, + { name = "pytest-metadata" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/c4/08/2076aa09507e51c1119d16a84c6307354d16270558f1a44fc9a2c99fdf1d/pytest_html-4.2.0.tar.gz", hash = "sha256:b6a88cba507500d8709959201e2e757d3941e859fd17cfd4ed87b16fc0c67912", size = 108634, upload-time = "2026-01-19T11:25:26.471Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/84/47/07046e0acedc12fe2bae79cf6c73ad67f51ae9d67df64d06b0f3eac73d36/pytest_html-4.2.0-py3-none-any.whl", hash = "sha256:ff5caf3e17a974008e5816edda61168e6c3da442b078a44f8744865862a85636", size = 23801, upload-time = "2026-01-19T11:25:25.008Z" }, +] + +[[package]] +name = "pytest-metadata" +version = "3.1.1" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "pytest-sugar" +version = "1.1.1" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "pytest" }, + { name = "termcolor" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/0b/4e/60fed105549297ba1a700e1ea7b828044842ea27d72c898990510b79b0e2/pytest-sugar-1.1.1.tar.gz", hash = "sha256:73b8b65163ebf10f9f671efab9eed3d56f20d2ca68bda83fa64740a92c08f65d", size = 16533, upload-time = "2025-08-23T12:19:35.737Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/87/d5/81d38a91c1fdafb6711f053f5a9b92ff788013b19821257c2c38c1e132df/pytest_sugar-1.1.1-py3-none-any.whl", hash = "sha256:2f8319b907548d5b9d03a171515c1d43d2e38e32bd8182a1781eb20b43344cc8", size = 11440, upload-time = "2025-08-23T12:19:34.894Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.apple.com/simple" } dependencies = [ - { name = "filelock" }, - { name = "platformdirs" }, + { name = "execnet" }, + { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/88/815e53084c5079a59df912825a279f41dd2e0df82281770eadc732f5352c/python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e", size = 58457, upload-time = "2026-03-26T22:30:44.496Z" } +sdist = { url = "https://pypi.apple.com/packages/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502", size = 31674, upload-time = "2026-03-26T22:30:43.396Z" }, + { url = "https://pypi.apple.com/packages/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] [[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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "resolvelib" +version = "1.2.1" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/1d/14/4669927e06631070edb968c78fdb6ce8992e27c9ab2cde4b3993e22ac7af/resolvelib-1.2.1.tar.gz", hash = "sha256:7d08a2022f6e16ce405d60b68c390f054efcfd0477d4b9bd019cc941c28fad1c", size = 24575, upload-time = "2025-10-11T01:07:44.582Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/e2/23/c941a0d0353681ca138489983c4309e0f5095dfd902e1357004f2357ddf2/resolvelib-1.2.1-py3-none-any.whl", hash = "sha256:fb06b66c8da04172d9e72a21d7d06186d8919e32ae5ab5cdf5b9d920be805ac2", size = 18737, upload-time = "2025-10-11T01:07:43.081Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, ] [[package]] name = "ruff" -version = "0.15.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, - { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, - { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, - { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, - { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, - { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, - { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, - { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, +version = "0.15.20" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "shellcheck-py" +version = "0.11.0.1" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/df/55/455b097417b3df3d330eff029c72c32f08b25739e3010acb30ad06d268ef/shellcheck_py-0.11.0.1.tar.gz", hash = "sha256:5c620c88901e8f1d3be5934b31ea99e3310065e1245253741eafd0a275c8c9cc", size = 3139, upload-time = "2025-08-09T17:53:42.492Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/54/27/d75b03e5458cefdb6d3b674566cd20476c3e4d3fe6cc9d68b7e3b854b296/shellcheck_py-0.11.0.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:b6a3fee28efda2e16e38d6e6d59faf7224300256456639727370d404730849e8", size = 6774472, upload-time = "2025-08-09T17:53:34.573Z" }, + { url = "https://pypi.apple.com/packages/packages/61/ac/2a84c37171c0cf5a10ea4b0a27d43eb0a1d29bd98b49c2c5ffe17ad24bbe/shellcheck_py-0.11.0.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:6b88d0a244c82ed07e06a53e444da841f69330ca59ae15d4a66c391655dae7a0", size = 11381835, upload-time = "2025-08-09T17:53:36.852Z" }, + { url = "https://pypi.apple.com/packages/packages/96/55/250e0e3367613a5c22bd82e33b16b889287d81ab0f7dda67e6514a4cccf4/shellcheck_py-0.11.0.1-py2.py3-none-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b274df81de5b000ff78db433e7328b87e52e3c38481c60f8e488c3095beef05", size = 3800600, upload-time = "2025-08-09T17:53:38.643Z" }, + { url = "https://pypi.apple.com/packages/packages/15/5b/bb14c0a7474463b1aa3c09e866cb172dffc66ed2993b7ea8f1db581e86ee/shellcheck_py-0.11.0.1-py2.py3-none-win_amd64.whl", hash = "sha256:784156289ecb17e91c692cd783ab5152333309588cabb10032a047331c63e759", size = 8027541, upload-time = "2025-08-09T17:53:40.889Z" }, +] + +[[package]] +name = "shfmt-py" +version = "4.0.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/06/d5/c2ad5c6593a34da7344cf39bde65763e8cda752589074ba1619e55b317ad/shfmt_py-4.0.0.tar.gz", hash = "sha256:1e5fdacf40aabaa77a97639d52a6220df0893b46658d82b7f136f4e66e2b2fb0", size = 11947, upload-time = "2026-05-13T09:25:50.153Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/5a/1d/8f72824e2a0e06dc0bc2687baacaba0573be7d2e93c01d1e895fddd8c13e/shfmt_py-4.0.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:75a4919a03fb3bcff9795e3cc7b971e37e74905654d2f11605001cab42e5f92f", size = 1343695, upload-time = "2026-05-13T09:25:42.969Z" }, + { url = "https://pypi.apple.com/packages/packages/a8/82/9564a2c2a76fbec94db1b3a3c37a9a1d00e7eafca2cdd2e0d19082618d7e/shfmt_py-4.0.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:bb3d236163ff39c7790953e069938caf247e7646399f7a059f00f65d4e6916d6", size = 1237947, upload-time = "2026-05-13T09:25:44.767Z" }, + { url = "https://pypi.apple.com/packages/packages/9a/a9/6fce944efa530db941edd11388d70dc7384aaf12169a3b0847b6a6c987b0/shfmt_py-4.0.0-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:4701336c3cb5f3959a5e85481b14f02054ea094b3c666f3d04649bbe10de3c25", size = 1218771, upload-time = "2026-05-13T09:25:46.225Z" }, + { url = "https://pypi.apple.com/packages/packages/64/43/e3965a25bb39555f2791c6860214f62b6f976f9ac7e9786073364bcdd9a6/shfmt_py-4.0.0-py2.py3-none-manylinux2014_x86_64.whl", hash = "sha256:e57877abe0177a9da7bbb5390fe7e96aa19b00958189a025634039aef8834d44", size = 1350939, upload-time = "2026-05-13T09:25:47.584Z" }, + { url = "https://pypi.apple.com/packages/packages/95/20/db2430d9262d2cffadcad2b330441e13031f1ab849ec069659edb7f23257/shfmt_py-4.0.0-py2.py3-none-win_amd64.whl", hash = "sha256:bd4f3d36264d4ba8b014ff73e5e702aaa2345845c021f563480128de3705135b", size = 1427721, upload-time = "2026-05-13T09:25:48.865Z" }, ] [[package]] name = "tabulate" version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -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/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { 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" }, +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "taplo" +version = "0.9.3" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/71/79/513513960377e1212a28446acb323cf77dfce162e825a822f035b02a422d/taplo-0.9.3.tar.gz", hash = "sha256:6b73b45b9adbd20189d8981ac9055d5465227c58bbe1b0646a7588a1a5c07a1a", size = 102556, upload-time = "2024-08-19T10:22:15.005Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/61/42/a93c18ebb7cf3ee2a7a30dd2fda654aca458956c3b64bdfb9d82b2c42679/taplo-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1c3db689406d538420c64aa779ac8694cf44c13a46e158d6df406de65980b9c7", size = 4248497, upload-time = "2024-08-19T10:21:59.954Z" }, + { url = "https://pypi.apple.com/packages/packages/82/d2/f5b6e4a4f474f9fe613b5b91012520c3f62e46748a6ce9fd61fc2fb52fa2/taplo-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1e7782f33f97e7aa658d18788748bce5cf3ce440eeb419cf5861cf542740e610", size = 4044421, upload-time = "2024-08-19T10:22:03.06Z" }, + { url = "https://pypi.apple.com/packages/packages/7d/32/4ac46ff15bb9d060f50ad31fb3a80aa8ee1e6ca500104ca8569f6fbdee3d/taplo-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29d4d7abfcc10bd536e5a43fe6ec2c1931507c1433e79df03ea22e1030611cb6", size = 4334420, upload-time = "2024-08-19T10:22:05.68Z" }, + { url = "https://pypi.apple.com/packages/packages/ef/cc/656aed22a59cf4c50dcaaa66aaa570d4a1412acdd8ea429120a6bb00f336/taplo-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f12648f273478d7330cb3529c82f48f388501e1122e0bea78bce5ff5972b8b", size = 4468935, upload-time = "2024-08-19T10:22:08.106Z" }, + { url = "https://pypi.apple.com/packages/packages/21/15/d8db1db6382b444122fa1a66fe5fe0dd5b04bfbe68c74bdb5345aec11eb2/taplo-0.9.3-py3-none-win32.whl", hash = "sha256:9ab7df76a3facc6d0dd2fe2dae3e8eb52fa458d31d27878d5eac14f5cbc0abac", size = 3482612, upload-time = "2024-08-19T10:22:10.904Z" }, + { url = "https://pypi.apple.com/packages/packages/42/3c/df6641d7e2e84a6dd4de3b3a4426db7f6a7270c05bbdeadd523645c9c45f/taplo-0.9.3-py3-none-win_amd64.whl", hash = "sha256:7d80b630b93fb43cee99d1e1ee07b616236dc5615efaf7cd51074b4cffc33bab", size = 3985843, upload-time = "2024-08-19T10:22:13.446Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "trove-classifiers" +version = "2026.6.1.19" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/c2/e3/7ca82ee24c82d344584abd5b8637b3bd056f2900226e8d82fc22f1184b92/trove_classifiers-2026.6.1.19.tar.gz", hash = "sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745", size = 17059, upload-time = "2026-06-01T19:41:34.649Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3", size = 14211, upload-time = "2026-06-01T19:41:33.434Z" }, ] [[package]] name = "types-pyyaml" -version = "6.0.12.20250915" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +version = "6.0.12.20260518" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, + { url = "https://pypi.apple.com/packages/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, ] [[package]] name = "types-requests" -version = "2.33.0.20260327" -source = { registry = "https://pypi.org/simple" } +version = "2.33.0.20260518" +source = { registry = "https://pypi.apple.com/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/5f/2e3dbae6e21be6ae026563bad96cbf76602d73aa85ea09f13419ddbdabb4/types_requests-2.33.0.20260327.tar.gz", hash = "sha256:f4f74f0b44f059e3db420ff17bd1966e3587cdd34062fe38a23cda97868f8dd8", size = 23804, upload-time = "2026-03-27T04:23:38.737Z" } +sdist = { url = "https://pypi.apple.com/packages/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/55/951e733616c92cb96b57554746d2f65f4464d080cc2cc093605f897aba89/types_requests-2.33.0.20260327-py3-none-any.whl", hash = "sha256:fde0712be6d7c9a4d490042d6323115baf872d9a71a22900809d0432de15776e", size = 20737, upload-time = "2026-03-27T04:23:37.813Z" }, + { url = "https://pypi.apple.com/packages/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" }, +] + +[[package]] +name = "types-toml" +version = "0.10.8.20260518" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/4b/11/6ece999e91f2ccb848ab4420f3f4816e78ac0541f739e6864affdaaa5737/types_toml-0.10.8.20260518.tar.gz", hash = "sha256:80e10facd24fdeda9d5c672187d72be3ac284843788d67f5aae59e3e016db6fe", size = 9419, upload-time = "2026-05-18T06:02:16.719Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/91/25/489751806bf5c95e4007f8e17409199c54d31e49ffbea07c5729b1286c8e/types_toml-0.10.8.20260518-py3-none-any.whl", hash = "sha256:0e564ab05f6fde62a315b3b5a9b6624fda569399795d30a37e64705a70459303", size = 9669, upload-time = "2026-05-18T06:02:15.86Z" }, ] [[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" } +version = "4.16.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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" -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" } +version = "2.7.0" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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.2.0" -source = { registry = "https://pypi.org/simple" } +name = "validate-pyproject" +version = "0.25" +source = { registry = "https://pypi.apple.com/simple" } dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, - { name = "python-discovery" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "fastjsonschema" }, +] +sdist = { url = "https://pypi.apple.com/packages/packages/e7/d8/45a82408619c306d0083afcf6142617201b2647a82e9a7d4c902e837274c/validate_pyproject-0.25.tar.gz", hash = "sha256:e68c12d1cb0d8ddc269ffc42875a81727ddb7865000aa6d2f77d833b55c53f0b", size = 118662, upload-time = "2026-02-02T17:31:07.089Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl", hash = "sha256:f9d05e2686beff82f9ea954f582306b036ced3d3feb258c1110f2c2a495b1981", size = 54942, upload-time = "2026-02-02T17:31:05.489Z" }, +] + +[package.optional-dependencies] +all = [ + { name = "packaging" }, + { name = "trove-classifiers" }, +] + +[[package]] +name = "validate-pyproject-schema-store" +version = "2026.7.8" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/packages/37/1a/56d4924eed352c36828358f33e814f2a8bc9dcf28d41a9136a9af3462f9b/validate_pyproject_schema_store-2026.7.8.tar.gz", hash = "sha256:08eb8624b4a020e3bffe9eba2fc1e3123b97e6a159ef38c76d36d33cb45508ca", size = 191745, upload-time = "2026-07-08T10:32:05.15Z" } +wheels = [ + { url = "https://pypi.apple.com/packages/packages/77/9a/881842d94b6ba4ce9e56e5771b7b8ea4a69b67f0e994e8ecc8de4b7d8c50/validate_pyproject_schema_store-2026.7.8-py3-none-any.whl", hash = "sha256:8eb021f774b91926a2a194ddcd1cdaaa1b716b8031434fb099bf9b0bf03f8a89", size = 198162, upload-time = "2026-07-08T10:32:03.432Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.8.2" +source = { registry = "https://pypi.apple.com/simple" } +sdist = { url = "https://pypi.apple.com/packages/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://pypi.apple.com/packages/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 = "yamllint" +version = "1.38.0" +source = { registry = "https://pypi.apple.com/simple" } +dependencies = [ + { name = "pathspec" }, + { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } +sdist = { url = "https://pypi.apple.com/packages/packages/28/a0/8fc2d68e132cf918f18273fdc8a1b8432b60d75ac12fdae4b0ef5c9d2e8d/yamllint-1.38.0.tar.gz", hash = "sha256:09e5f29531daab93366bb061e76019d5e91691ef0a40328f04c927387d1d364d", size = 142446, upload-time = "2026-01-13T07:47:53.276Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, + { url = "https://pypi.apple.com/packages/packages/05/92/aed08e68de6e6a3d7c2328ce7388072cd6affc26e2917197430b646aed02/yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220", size = 68940, upload-time = "2026-01-13T07:47:51.343Z" }, ]