diff --git a/.ansible-lint b/.ansible-lint index f3c14d9..6299a17 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -5,3 +5,6 @@ exclude_paths: - molecule/ - .molecule/ - .pytest_cache/ +skip_list: + # Rootless Docker uses `systemctl --user` which the systemd module doesn't support + - command-instead-of-module diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 78efd8f..00eb445 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -36,22 +36,32 @@ jobs: - uses: actions/checkout@v4 - name: Set up environment run: make setup - - name: Discover assigned scenarios + - name: Discover assigned test pairs run: | . .venv/bin/activate - SCENARIOS=$(python3 scripts/distribute_molecule.py --runner-index ${{ matrix.runner-index }} --max-runners 3) - echo "Assigned scenarios: $SCENARIOS" - echo "SCENARIOS=$SCENARIOS" >> $GITHUB_ENV + PAIRS=$(python3 scripts/distribute_molecule.py --runner-index ${{ matrix.runner-index }} --max-runners 3) + echo "Assigned pairs: $PAIRS" + echo "TEST_PAIRS=$PAIRS" >> $GITHUB_ENV - name: Run molecule tests run: | . .venv/bin/activate export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock" export ANSIBLE_INJECT_INVOCATION=1 cd ansible/roles/gitea-runner - for s in $SCENARIOS; do - if [ "$s" = "default" ]; then + for pair in $TEST_PAIRS; do + IFS='|' read -r scenario platform_name platform_image platform_command <<< "$pair" + export MOLECULE_PLATFORM_NAME="$platform_name" + export MOLECULE_PLATFORM_IMAGE="$platform_image" + if [ -n "$platform_command" ]; then + export MOLECULE_PLATFORM_COMMAND="$platform_command" + else + unset MOLECULE_PLATFORM_COMMAND + fi + echo "::group::Molecule: $scenario on $platform_name" + if [ "$scenario" = "default" ]; then ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true molecule test else - ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true molecule test -s "$s" + ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true molecule test -s "$scenario" fi + echo "::endgroup::" done diff --git a/.python-version b/.python-version index 2d4715b..e4fba21 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11.11 +3.12 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36bbcf9..992696a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,7 +46,7 @@ make test-all # Runs pytest-cov + molecule make lint-all # Runs ruff, pyright, bandit, ansible-lint, checkmake make lint-bandit # Security scan with bandit make pytest-cov # Unit tests with 100% coverage enforcement -make molecule # All 7 molecule scenarios +make molecule # All 6 molecule scenarios ``` ## Code Quality diff --git a/Makefile b/Makefile index 072d80c..8f81fac 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all setup install update lint ansible-lint makefile-lint lint-all test test-unit pytest-cov molecule molecule-docker molecule-binary molecule-all test-all clean +.PHONY: all setup install update lint ansible-lint makefile-lint lint-all test test-unit pytest-cov molecule molecule-all test-all clean PYTHON := python3 VENV := .venv @@ -17,7 +17,7 @@ setup: $(VENV)/bin/activate .env activate-scripts checkmake fi $(VENV)/bin/activate: - @python3 -c "import sys; v=sys.version_info; assert v >= (3, 11), f'Python 3.11+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')" + @python3 -c "import sys; v=sys.version_info; assert v >= (3, 12), f'Python 3.12+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')" $(PYTHON) -m venv $(VENV) $(BIN)/pip install --upgrade pip setuptools wheel @@ -36,35 +36,35 @@ checkmake: install: @if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make install HOST=192.168.1.10"; exit 1; fi - $(BIN)/grm install $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(NAME),--name $(NAME),) $(if $(TOKEN),--token $(TOKEN),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) + $(BIN)/grm install $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(NAME),--name $(NAME),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) update: @if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make update HOST=192.168.1.10"; exit 1; fi - $(BIN)/grm update $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(VERSION),--version $(VERSION),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) + $(BIN)/grm update $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(VERSION),--version $(VERSION),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) start: @if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make start HOST=192.168.1.10"; exit 1; fi - $(BIN)/grm start $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) + $(BIN)/grm start $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) stop: @if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make stop HOST=192.168.1.10"; exit 1; fi - $(BIN)/grm stop $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) + $(BIN)/grm stop $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) enable: @if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make enable HOST=192.168.1.10"; exit 1; fi - $(BIN)/grm enable $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) + $(BIN)/grm enable $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) disable: @if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make disable HOST=192.168.1.10"; exit 1; fi - $(BIN)/grm disable $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(TOKEN),--token $(TOKEN),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) + $(BIN)/grm disable $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) status: @if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make status HOST=192.168.1.10"; exit 1; fi - $(BIN)/grm status $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) + $(BIN)/grm status $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) remove: @if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make remove HOST=192.168.1.10"; exit 1; fi - $(BIN)/grm remove $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(TOKEN),--token $(TOKEN),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) + $(BIN)/grm remove $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,) lint-ruff: $(BIN)/ruff check src/ tests/ @@ -98,22 +98,15 @@ pytest-cov: $(BIN)/pytest tests/unit/ -v --cov=src/gitea_runner_manager --cov=scripts --cov-report=term-missing --cov-fail-under=100 MOLECULE := $(realpath $(BIN))/molecule -MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea-runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true $(MOLECULE) +MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea-runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 $(MOLECULE) -# Quick local test: Ubuntu 22.04 only +# Quick local test: Ubuntu 22.04 only, all scenarios molecule: - @set -e; for s in default binary multi-instance lifecycle template-content deregister update; do if [ "$$s" = "default" ]; then $(MOLECULE_BASE) test; else $(MOLECULE_BASE) test -s $$s; fi; done + @set -e; for s in default multi-instance lifecycle template-content deregister update; do if [ "$$s" = "default" ]; then $(MOLECULE_BASE) test; else $(MOLECULE_BASE) test -s $$s; fi; done -# Docker mode tests for all platforms (sequential; override env vars for CI matrix parallelisation) -molecule-docker: - @set -e; for p in ubuntu-2204:geerlingguy/docker-ubuntu2204-ansible:latest:'' ubuntu-2404:geerlingguy/docker-ubuntu2404-ansible:latest:'' ubuntu-2604:geerlingguy/docker-ubuntu2604-ansible:latest:'' debian-12:geerlingguy/docker-debian12-ansible:latest:'' debian-13:geerlingguy/docker-debian13-ansible:latest:'' archlinux:marcstraube/archlinux-ansible:latest:/usr/lib/systemd/systemd; do IFS=":"; set -- $$p; MOLECULE_PLATFORM_NAME=$$1 MOLECULE_PLATFORM_IMAGE=$$2 MOLECULE_PLATFORM_COMMAND=$$3 $(MOLECULE_BASE) test; done - -# Binary mode tests for all platforms (sequential; override env vars for CI matrix parallelisation) -molecule-binary: - @set -e; for p in ubuntu-2204:geerlingguy/docker-ubuntu2204-ansible:latest:'' ubuntu-2404:geerlingguy/docker-ubuntu2404-ansible:latest:'' ubuntu-2604:geerlingguy/docker-ubuntu2604-ansible:latest:'' debian-12:geerlingguy/docker-debian12-ansible:latest:'' debian-13:geerlingguy/docker-debian13-ansible:latest:'' archlinux:marcstraube/archlinux-ansible:latest:/usr/lib/systemd/systemd; do IFS=":"; set -- $$p; MOLECULE_PLATFORM_NAME=$$1 MOLECULE_PLATFORM_IMAGE=$$2 MOLECULE_PLATFORM_COMMAND=$$3 $(MOLECULE_BASE) test -s binary; done - -# All platform tests (sequential; use CI matrix for parallel execution) -molecule-all: molecule-docker molecule-binary +# All scenarios on all supported platforms (sequential; use CI matrix for parallel execution) +molecule-all: + @bash scripts/molecule_all.sh test: test-all diff --git a/README.md b/README.md index 47b7fbe..d10646c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A lean command-line tool to automate the installation, configuration, and lifecycle management of Gitea Actions runners on Arch Linux, Ubuntu, and Debian hosts. -By default, runners are deployed as **Docker containers** using the official `gitea/runner` image. A traditional **binary** deployment mode is also available. +Each runner runs in an isolated **rootless Docker** environment under a dedicated system user, enabling multiple runners to operate in parallel on the same host without conflicts. The runner binary (`gitea_runner`) is installed directly and managed as a systemd user service. > **Pronunciation note:** GRM is short for *Gitea Runner Manager*, but say it like **ГРЪМ** (roughly "GRUM" in Latin letters) — the Bulgarian word for **thunder**. Wherever there are clouds, there may be thunders. This is an open-source project from **Oblachno** (облачно means *cloudy* in Bulgarian). @@ -23,19 +23,22 @@ This project uses **conventional commits** and **GRM-N branch prefixes**. See [C - **Flexible** — accepts a plain IP address or hostname, and allows specifying the SSH user and private key. - **Runner registry** — stores runner connection metadata locally after installation. Subsequent commands need only the runner name. - **Lifecycle management** — start, stop, enable, disable, status, and remove runners via CLI. -- **Multi-instance** — run multiple isolated runners on the same host, each with its own data directory and systemd service. -- **Systemd-managed** — both Docker and binary modes run under systemd template units (`gitea-runner@.service`). +- **Multi-instance** — run multiple isolated runners on the same host, each with its own system user, rootless Docker daemon, data directory, and systemd user service. +- **Rootless Docker** — each runner gets its own rootless Docker daemon, avoiding conflicts with the host's Docker installation and enabling true parallel execution. +- **Systemd-managed** — runners run as systemd user services (`gitea-runner.service`) under dedicated per-runner system users. ## Supported Operating Systems - Arch Linux -- Ubuntu 22.04 / 24.04 / 26.04 -- Debian 12 / 13 +- Ubuntu 22.04 / 24.04 +- Debian 12 + +All supported OSes are tested in CI via molecule scenarios on every PR. ## Prerequisites - **SSH key authentication** — The remote host must be reachable via SSH using the user specified with `--user` and the private key specified with `--key`. GRM uses Ansible under the hood, which connects to the target host over SSH to execute all installation and configuration tasks. Without valid SSH credentials, Ansible cannot establish a connection and the deployment will fail. -- **Sudo access** — GRM requires root privileges on the remote host to install packages, create systemd services, and manage Docker. By default, you will be prompted interactively for the sudo password. For automation or uninterrupted workflows, configure passwordless sudo on the remote host and pass `--no-ask-become-pass`. +- **Sudo access** — GRM requires root privileges on the remote host to create system users, install packages, and configure rootless Docker. By default, you will be prompted interactively for the sudo password. For automation or uninterrupted workflows, configure passwordless sudo on the remote host and pass `--no-ask-become-pass`. ## Quick Start @@ -44,8 +47,8 @@ This project uses **conventional commits** and **GRM-N branch prefixes**. See [C ```bash git clone https://git.oblachno.oblachno.com/oblachno/gitea-runner-manager.git cd gitea-runner-manager -pyenv install 3.11.11 -pyenv local 3.11.11 +pyenv install 3.12 +pyenv local 3.12 make setup ``` @@ -65,7 +68,7 @@ cp .env.example .env Set `GITEA_ADMIN_TOKEN` to enable informational API checks during integration test. This is **optional** — the test primarily verifies the runner by checking: 1. **`.runner` registration file** exists and contains valid JSON (proves successful registration) -2. **Container/service** is running (proves daemon is polling for jobs) +2. **Systemd user service** is active (proves daemon is polling for jobs) API checks, if enabled, are purely informational and do not affect pass/fail. @@ -74,11 +77,7 @@ API checks, if enabled, are purely informational and do not affect pass/fail. Using the CLI (you will be prompted for the sudo password by default): ```bash -# Docker mode (default) — deploys gitea_runner as a container grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner - -# Binary mode — downloads and installs the gitea_runner binary with systemd -grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner --mode binary ``` > **Automation tip:** Configure passwordless sudo on the remote host and pass `--no-ask-become-pass` to skip the password prompt. This is recommended for CI/CD pipelines. @@ -86,16 +85,12 @@ grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runne Using Make: ```bash -# Docker mode (default) make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner - -# Binary mode -make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner MODE=binary ``` ### Runner Registry -After installation, GRM stores each runner's connection details (host, user, SSH key, mode, Gitea URL) in a local JSON registry at `~/.local/share/grm/runners.json`. This means you rarely need to repeat connection arguments: +After installation, GRM stores each runner's connection details (host, user, SSH key, Gitea URL) in a local JSON registry at `~/.local/share/grm/runners.json`. This means you rarely need to repeat connection arguments: ```bash # List all registered runners with live systemd status @@ -129,19 +124,19 @@ grm remove prod-runner --token You can override any stored value by passing the corresponding flag: ```bash -grm start prod-runner --host 192.168.1.11 --user root --mode binary +grm start prod-runner --host 192.168.1.11 --user root ``` > **Automation tip:** If the remote host has passwordless sudo configured, pass `--no-ask-become-pass`. ### Multiple Instances on the Same Host -Each runner instance is fully isolated with its own data directory and systemd service: +Each runner instance is fully isolated with its own system user, rootless Docker daemon, data directory, and systemd user service: ```bash # Install two runners on the same host grm install 192.168.1.10 --user ubuntu --name workflow-runner -grm install 192.168.1.10 --user ubuntu --name build-runner --mode binary +grm install 192.168.1.10 --user ubuntu --name build-runner # Manage them independently by name grm stop workflow-runner @@ -153,7 +148,7 @@ grm status build-runner The installer performs an automated integration test that verifies: 1. **`.runner` file exists** with valid JSON containing `id`, `uuid`, `token`, `address` — this proves successful registration with Gitea -2. **Container/service is running** — this proves the daemon is active and polling for jobs +2. **Systemd user service is active** — this proves the daemon is polling for jobs You can also check the Gitea UI under **Actions → Runners** to confirm the runner appears as **Online**. @@ -174,11 +169,8 @@ GRM_LOG_LEVEL=DEBUG grm install 192.168.1.10 --user ubuntu --name prod-runner **Runner logs** (on the remote host): ```bash -# Binary mode logs (via systemd template unit) -sudo journalctl -u gitea-runner@ -f - -# Docker mode logs -docker logs gitea-runner- -f +# Runner logs (via systemd user service) +sudo -u grm- journalctl --user -u gitea-runner -f ``` The GRM application writes to two destinations: @@ -197,27 +189,28 @@ Console output is automatically colorised via ``click.echo``: operation headers GRM consists of two layers: 1. **Python CLI** (`src/gitea_runner_manager/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. -2. **Ansible Role** (`ansible/roles/gitea-runner/`) — idempotent role that installs Docker, configures the runner (binary or container), creates systemd template units, and registers the runner with Gitea. +2. **Ansible Role** (`ansible/roles/gitea-runner/`) — idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, and registers the runner with Gitea. ``` grm install └── RunnerManager.install() └── ansible-playbook ansible/install-runner.yml └── role: gitea-runner - ├── docker.yml (Docker installation) - ├── docker_mode.yml (container deployment) - ├── binary_mode.yml (binary + systemd deployment) - ├── service.yml (systemd template unit) - ├── deregister.yml (deregistration from Gitea) - ├── prune.yml (Docker prune timer) - └── integration_test.yml (validate online status) + ├── user_setup.yml (create per-runner system user + lingering) + ├── rootless_docker.yml (rootless Docker setup under runner user) + ├── install_runner.yml (download binary, config, register, service) + ├── prune.yml (Docker prune timer) + └── integration_test.yml (validate service is active) ``` -Both Docker and binary modes run under a single systemd template unit (`gitea-runner@.service`), instantiated per runner name (e.g., `gitea-runner@prod-runner.service`). Each instance has fully isolated directories: +Each runner runs as a systemd user service under a dedicated system user (`grm-`). Each instance has fully isolated resources: +- **User**: `grm-` (dedicated system user with lingering enabled) +- **Home**: `/home/grm-/` - **Data**: `/var/lib/gitea-runner//` -- **Config**: `/etc/gitea-runner//` (binary mode) -- **Service**: `gitea-runner@.service` +- **Config**: `/etc/gitea-runner//` +- **Service**: `gitea-runner.service` (systemd user service) +- **Docker socket**: `/run/user//docker.sock` (rootless, per-runner) ## Configuration @@ -225,11 +218,13 @@ All tunable values are exposed as Ansible variables in `ansible/roles/gitea-runn | Variable | Default | Description | |----------|---------|-------------| -| `runner_mode` | `docker` | Deployment mode: `docker` or `binary` | -| `gitea_runner_version` | `1.0.8` | Docker image tag / binary version | -| `gitea_runner_docker_image` | `gitea/runner` | Docker image name | +| `gitea_runner_version` | `1.0.8` | Runner binary version | | `runner_labels` | `ubuntu-latest:docker://runner-images:ubuntu-22.04` | Runner labels | | `skip_runner_registration` | `false` | Skip API registration (useful for tests) | +| `gitea_runner_user_prefix` | `grm-` | Prefix for per-runner system users | +| `gitea_runner_base_home` | `/home` | Base directory for runner user homes | +| `gitea_runner_service_user` | `{{ prefix }}{{ runner_name }}` | Per-runner system user | +| `gitea_runner_home` | `{{ base_home }}/{{ service_user }}` | Runner user home directory | | `gitea_runner_base_data_dir` | `/var/lib/gitea-runner` | Base data directory (instance-scoped) | | `gitea_runner_base_config_dir` | `/etc/gitea-runner` | Base config directory (instance-scoped) | | `gitea_runner_data_dir` | `{{ base }}/{{ runner_name }}` | Runtime data directory per instance | @@ -239,7 +234,6 @@ All tunable values are exposed as Ansible variables in `ansible/roles/gitea-runn | `gitea_runner_prune_schedule` | `daily` | systemd timer schedule | | `gitea_runner_prune_label` | `gitea-runner=true` | Docker label for pruning | | `gitea_runner_service_restart_sec` | `5` | systemd RestartSec value | -| `gitea_runner_service_user` | `{{ ansible_user \| default('root') }}` | Service user | | `gitea_runner_log_level` | `info` | Runner log level | | `gitea_runner_container_label` | `gitea-runner=true` | Container label | | `docker_gpg_key_path` | `/etc/apt/keyrings/docker.asc` | Docker GPG key path | @@ -311,18 +305,23 @@ Runs pytest with 100% coverage requirement. ### Molecule Tests ```bash -make molecule +make molecule # Quick: all 6 scenarios on Ubuntu 22.04 +make molecule-all # Full: all 6 scenarios on all 4 supported OSes ``` -Runs four scenarios: +Runs six scenarios: -- **default** — Docker mode installation in an Ubuntu 22.04 container -- **binary** — Binary mode installation in an Ubuntu 22.04 container +- **default** — Rootless Docker runner installation - **multi-instance** — Two isolated runner instances on the same host - **lifecycle** — Stop, disable, re-enable, and start sequence +- **template-content** — Verify rendered systemd user service and prune templates +- **deregister** — Runner deregistration +- **update** — Runner binary update All scenarios test idempotence (second run produces zero changes). +CI runs all 6 scenarios × 4 platforms (24 test pairs) distributed across 3 parallel runners. + ### Integration Tests ```bash @@ -346,7 +345,7 @@ This is a harmless cleanup traceback from Molecule's Docker driver when the test ### Runner appears offline after installation - Check that the `GITEA_URL` and `GITEA_REGISTRATION_TOKEN` environment variables are correct. -- Verify the runner container or service is running: `docker ps` or `systemctl status gitea-runner@`. +- Verify the runner service is running: `sudo -u grm- systemctl --user status gitea-runner`. - Check logs for registration errors. ### Integration test fails @@ -358,22 +357,17 @@ The test checks two things: - Runner logs for registration errors - The `.runner` file should exist at `/var/lib/gitea-runner//.runner` -2. **Container/service not running** — Daemon failed to start. Check: - - `docker ps` or `systemctl status gitea-runner@` +2. **Service not running** — Daemon failed to start. Check: + - `sudo -u grm- systemctl --user status gitea-runner` - Logs for connection errors -### Docker mode: container won't start +### Rootless Docker: service fails to start -- Ensure Docker is installed and running on the host. -- Verify the Docker socket is accessible: `docker version`. -- Check systemd status: `systemctl status gitea-runner@`. - -### Binary mode: systemd service fails - -- Check the service status: `systemctl status gitea-runner@`. -- Verify the binary exists at `gitea_runner_binary_path`. -- Ensure the service user is in the `docker` group. -- Check logs: `journalctl -u gitea-runner@ -f`. +- Check the service status: `sudo -u grm- systemctl --user status gitea-runner`. +- Verify the rootless Docker daemon is running: `sudo -u grm- systemctl --user status docker`. +- Verify the Docker socket exists: `ls /run/user/$(id -u grm-)/docker.sock`. +- Check logs: `sudo -u grm- journalctl --user -u gitea-runner -f`. +- Ensure lingering is enabled for the runner user: `loginctl show-user grm- | grep Linger`. ## Makefile Targets diff --git a/ansible/disable-runner.yml b/ansible/disable-runner.yml index e57c55c..8dbda60 100644 --- a/ansible/disable-runner.yml +++ b/ansible/disable-runner.yml @@ -9,12 +9,14 @@ name: gitea-runner tasks_from: systemd_check.yml - - name: Stop gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner@{{ runner_name }}" - state: stopped - daemon_reload: true + - name: Stop gitea-runner user service + ansible.builtin.command: systemctl --user stop gitea-runner + become: true + become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" when: systemd_available.stat.exists + changed_when: true - name: Include deregistration ansible.builtin.include_role: @@ -22,9 +24,11 @@ tasks_from: deregister.yml when: not skip_runner_registration | default(false) - - name: Disable gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner@{{ runner_name }}" - enabled: false - daemon_reload: true + - name: Disable gitea-runner user service + ansible.builtin.command: systemctl --user disable gitea-runner + become: true + become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" when: systemd_available.stat.exists + changed_when: true diff --git a/ansible/enable-runner.yml b/ansible/enable-runner.yml index f2b1722..b5913f4 100644 --- a/ansible/enable-runner.yml +++ b/ansible/enable-runner.yml @@ -9,9 +9,20 @@ name: gitea-runner tasks_from: systemd_check.yml - - name: Enable gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner@{{ runner_name }}" - enabled: true - daemon_reload: true + - name: Enable gitea-runner user service + ansible.builtin.command: systemctl --user enable gitea-runner + become: true + become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" when: systemd_available.stat.exists + changed_when: true + + - name: Start gitea-runner user service + ansible.builtin.command: systemctl --user start gitea-runner + become: true + become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" + when: systemd_available.stat.exists + changed_when: true diff --git a/ansible/inventory.example b/ansible/inventory.example index a505d00..c384c23 100644 --- a/ansible/inventory.example +++ b/ansible/inventory.example @@ -6,9 +6,8 @@ # ansible_ssh_private_key_file — Path to SSH private key # # Optional variables per host: -# runner_mode=docker — Deployment mode: docker or binary -# gitea_runner_version=1.0.8 — Runner image/binary version +# gitea_runner_version=1.0.8 — Runner binary version [runners] 192.168.1.10 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_ed25519 -runner.example.com ansible_user=arch ansible_ssh_private_key_file=~/.ssh/id_ed25519 runner_mode=binary +runner.example.com ansible_user=arch ansible_ssh_private_key_file=~/.ssh/id_ed25519 diff --git a/ansible/remove-runner.yml b/ansible/remove-runner.yml index 37c0846..fe17f0e 100644 --- a/ansible/remove-runner.yml +++ b/ansible/remove-runner.yml @@ -9,31 +9,45 @@ name: gitea-runner tasks_from: systemd_check.yml - - name: Stop gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner-{{ runner_mode | default('docker') }}@{{ runner_name }}" - state: stopped - daemon_reload: true - when: systemd_available.stat.exists + - name: Get runner user UID + ansible.builtin.command: id -u "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + register: runner_uid_result + changed_when: false + failed_when: false - - name: Disable gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner-{{ runner_mode | default('docker') }}@{{ runner_name }}" - enabled: false - daemon_reload: true - when: systemd_available.stat.exists + - name: Set runner UID fact + ansible.builtin.set_fact: + gitea_runner_uid: "{{ runner_uid_result.stdout }}" + when: runner_uid_result.rc == 0 - - name: Stop Docker container directly - ansible.builtin.command: "docker stop -t 30 gitea-runner-{{ runner_name }}" + - name: Stop gitea-runner user service + ansible.builtin.command: systemctl --user stop gitea-runner + become: true + become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" + when: systemd_available.stat.exists changed_when: true failed_when: false - when: runner_mode | default('docker') == 'docker' - - name: Remove Docker container directly - ansible.builtin.command: "docker rm -f gitea-runner-{{ runner_name }}" + - name: Disable gitea-runner user service + ansible.builtin.command: systemctl --user disable gitea-runner + become: true + become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" + when: systemd_available.stat.exists + changed_when: true + failed_when: false + + - name: Stop rootless Docker daemon + ansible.builtin.command: systemctl --user stop docker + become: true + become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" changed_when: true failed_when: false - when: runner_mode | default('docker') == 'docker' - name: Include deregistration ansible.builtin.include_role: @@ -41,12 +55,30 @@ tasks_from: deregister.yml when: not skip_runner_registration | default(false) - - name: Remove systemd unit file (last instance) + - name: Remove systemd user unit file ansible.builtin.file: - path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service" + path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/gitea-runner.service" state: absent when: remove_systemd_template | default(false) + - name: Kill remaining processes of runner user + ansible.builtin.command: loginctl terminate-user "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + failed_when: false + changed_when: true + + - name: Wait for processes to terminate + ansible.builtin.command: "pkill -u {{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + failed_when: false + changed_when: false + + - name: Remove runner user and home directory + ansible.builtin.user: + name: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + state: absent + remove: true + when: remove_runner_user | default(true) + failed_when: false + - name: Remove runner data directory ansible.builtin.file: path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ runner_name) }}" diff --git a/ansible/roles/gitea-runner/defaults/main.yml b/ansible/roles/gitea-runner/defaults/main.yml index eed3ea6..d0b41ad 100644 --- a/ansible/roles/gitea-runner/defaults/main.yml +++ b/ansible/roles/gitea-runner/defaults/main.yml @@ -1,10 +1,14 @@ --- -runner_mode: "docker" gitea_runner_version: "1.0.8" -gitea_runner_docker_image: "gitea/runner" -runner_labels: "ubuntu-latest:docker://runner-images:ubuntu-22.04" +runner_labels: "docker,ubuntu-latest:docker://runner-images:ubuntu-22.04" skip_runner_registration: false +# Per-runner user (rootless isolation) +gitea_runner_user_prefix: "grm-" +gitea_runner_base_home: "/home" +gitea_runner_service_user: "{{ gitea_runner_user_prefix }}{{ runner_name }}" +gitea_runner_home: "{{ gitea_runner_base_home }}/{{ gitea_runner_service_user }}" + # Base paths (instance-scoped via runner_name) gitea_runner_base_data_dir: "/var/lib/gitea-runner" gitea_runner_base_config_dir: "/etc/gitea-runner" @@ -19,12 +23,11 @@ gitea_runner_prune_label: "gitea-runner=true" # Service configuration gitea_runner_service_restart_sec: "5" -gitea_runner_service_user: "{{ ansible_user | default('root') }}" # Runner configuration gitea_runner_log_level: "info" gitea_runner_container_label: "gitea-runner=true" gitea_runner_file: ".runner" -# Docker installation +# Docker installation (for rootless dependencies) docker_gpg_key_path: "/etc/apt/keyrings/docker.asc" diff --git a/ansible/roles/gitea-runner/handlers/main.yml b/ansible/roles/gitea-runner/handlers/main.yml index 9a47d62..5130db1 100644 --- a/ansible/roles/gitea-runner/handlers/main.yml +++ b/ansible/roles/gitea-runner/handlers/main.yml @@ -5,7 +5,10 @@ when: ansible_facts is defined and ansible_facts['service_mgr'] | default('') == 'systemd' - name: Restart gitea-runner - ansible.builtin.systemd: - name: "gitea-runner-{{ runner_mode }}@{{ runner_name }}" - state: restarted + ansible.builtin.command: systemctl --user restart gitea-runner + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + changed_when: true when: ansible_facts is defined and ansible_facts['service_mgr'] | default('') == 'systemd' diff --git a/ansible/roles/gitea-runner/molecule/binary/converge.yml b/ansible/roles/gitea-runner/molecule/binary/converge.yml deleted file mode 100644 index 809a573..0000000 --- a/ansible/roles/gitea-runner/molecule/binary/converge.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -- name: Converge - hosts: all - become: true - vars: - gitea_url: "http://localhost:3000" - registration_token: "fake-token-for-testing" - runner_name: "molecule-test-runner" - runner_mode: "binary" - skip_runner_registration: true - roles: - - role: gitea-runner diff --git a/ansible/roles/gitea-runner/molecule/binary/molecule.yml b/ansible/roles/gitea-runner/molecule/binary/molecule.yml deleted file mode 100644 index 755e682..0000000 --- a/ansible/roles/gitea-runner/molecule/binary/molecule.yml +++ /dev/null @@ -1,39 +0,0 @@ ---- -driver: - name: docker - -platforms: - - name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204} - image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04} - command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity} - volumes: - - /sys/fs/cgroup:/sys/fs/cgroup:rw - cgroupns_mode: host - privileged: true - pre_build_image: false - -provisioner: - name: ansible - playbooks: - converge: converge.yml - prepare: ../common/prepare.yml - env: - ANSIBLE_ROLES_PATH: "../../.." - -scenario: - test_sequence: - - dependency - - cleanup - - destroy - - syntax - - create - - prepare - - converge - - idempotence - - side_effect - - verify - - cleanup - - destroy - -verifier: - name: ansible diff --git a/ansible/roles/gitea-runner/molecule/binary/verify.yml b/ansible/roles/gitea-runner/molecule/binary/verify.yml deleted file mode 100644 index f856bc6..0000000 --- a/ansible/roles/gitea-runner/molecule/binary/verify.yml +++ /dev/null @@ -1,72 +0,0 @@ ---- -- name: Verify - hosts: all - become: true - vars: - runner_name: "molecule-test-runner" - pre_tasks: - - name: Load role defaults - ansible.builtin.include_vars: - dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults" - - name: Override runner mode for binary scenario - ansible.builtin.set_fact: - runner_mode: "binary" - tasks: - - name: Check gitea_runner binary exists - ansible.builtin.stat: - path: "{{ gitea_runner_binary_path }}" - register: gitea_runner_stat - - - name: Assert gitea_runner binary exists - ansible.builtin.assert: - that: - - gitea_runner_stat.stat.exists - fail_msg: "gitea_runner binary is missing at {{ gitea_runner_binary_path }}" - - - name: Check Docker is installed - ansible.builtin.command: docker --version - changed_when: false - - - name: Check systemd template unit exists - ansible.builtin.stat: - path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service" - register: service_stat - - - name: Assert template unit exists - ansible.builtin.assert: - that: - - service_stat.stat.exists - fail_msg: "Systemd template unit is missing" - - - name: Check instance data directory exists - ansible.builtin.stat: - path: "{{ gitea_runner_data_dir }}" - register: data_dir_stat - - - name: Assert instance data directory exists - ansible.builtin.assert: - that: - - data_dir_stat.stat.exists - fail_msg: "Instance data directory is missing" - - - name: Check prune timer exists - ansible.builtin.stat: - path: /etc/systemd/system/docker-prune.timer - register: timer_stat - - - name: Assert prune timer exists - ansible.builtin.assert: - that: - - timer_stat.stat.exists - fail_msg: "Docker prune timer is missing" - - - name: Check config file exists - ansible.builtin.stat: - path: "{{ gitea_runner_config_dir }}/config.yaml" - register: config_stat - - - name: Assert config file exists - ansible.builtin.assert: - that: - - config_stat.stat.exists - fail_msg: "Config file is missing" diff --git a/ansible/roles/gitea-runner/molecule/common/prepare.yml b/ansible/roles/gitea-runner/molecule/common/prepare.yml index 1cbe51a..1286c67 100644 --- a/ansible/roles/gitea-runner/molecule/common/prepare.yml +++ b/ansible/roles/gitea-runner/molecule/common/prepare.yml @@ -19,16 +19,3 @@ community.general.pacman: update_cache: true when: ansible_facts['os_family'] == 'Archlinux' - - - name: Ensure /etc/docker directory exists - ansible.builtin.file: - path: /etc/docker - state: directory - mode: "0755" - - - name: Configure Docker daemon for vfs storage driver (DinD) - ansible.builtin.copy: - dest: /etc/docker/daemon.json - content: | - {"storage-driver": "vfs"} - mode: "0644" diff --git a/ansible/roles/gitea-runner/molecule/default/verify.yml b/ansible/roles/gitea-runner/molecule/default/verify.yml index 698014e..8bae1f6 100644 --- a/ansible/roles/gitea-runner/molecule/default/verify.yml +++ b/ansible/roles/gitea-runner/molecule/default/verify.yml @@ -9,42 +9,39 @@ ansible.builtin.include_vars: dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults" tasks: - - name: Check Docker is installed - ansible.builtin.command: docker --version - changed_when: false + - name: Check runner user exists + ansible.builtin.user: + name: "{{ gitea_runner_service_user }}" + register: user_info + check_mode: true - - name: Check runner image was pulled - ansible.builtin.command: docker images gitea/runner --format '{{ '{{' }} .Repository {{ '}}' }}:{{ '{{' }} .Tag {{ '}}' }}' - register: image_list - changed_when: false - - - name: Assert runner image exists locally + - name: Assert runner user exists ansible.builtin.assert: that: - - "'gitea/runner' in image_list.stdout" - fail_msg: "Runner Docker image was not pulled" + - user_info.state == "present" + fail_msg: "Runner system user was not created" - - name: Check prune timer exists + - name: Check runner binary exists ansible.builtin.stat: - path: /etc/systemd/system/docker-prune.timer - register: timer_stat + path: "{{ gitea_runner_binary_path }}" + register: binary_stat - - name: Assert prune timer exists + - name: Assert runner binary exists ansible.builtin.assert: that: - - timer_stat.stat.exists - fail_msg: "Docker prune timer is missing" + - binary_stat.stat.exists + fail_msg: "Gitea runner binary is missing" - - name: Check systemd template unit exists + - name: Check systemd user service exists ansible.builtin.stat: - path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service" + path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service" register: service_stat - - name: Assert template unit exists + - name: Assert user service exists ansible.builtin.assert: that: - service_stat.stat.exists - fail_msg: "Systemd template unit is missing" + fail_msg: "Systemd user service is missing" - name: Check instance data directory exists ansible.builtin.stat: @@ -57,9 +54,9 @@ - data_dir_stat.stat.exists fail_msg: "Instance data directory is missing" - - name: Check config file exists in data directory + - name: Check config file exists in config directory ansible.builtin.stat: - path: "{{ gitea_runner_data_dir }}/config.yaml" + path: "{{ gitea_runner_config_dir }}/config.yaml" register: config_stat - name: Assert config file exists @@ -67,3 +64,14 @@ that: - config_stat.stat.exists fail_msg: "Config file is missing" + + - name: Check prune timer exists + ansible.builtin.stat: + path: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer" + register: timer_stat + + - name: Assert prune timer exists + ansible.builtin.assert: + that: + - timer_stat.stat.exists + fail_msg: "Docker prune timer is missing" diff --git a/ansible/roles/gitea-runner/molecule/deregister/side_effect.yml b/ansible/roles/gitea-runner/molecule/deregister/side_effect.yml index 305cfe0..999181b 100644 --- a/ansible/roles/gitea-runner/molecule/deregister/side_effect.yml +++ b/ansible/roles/gitea-runner/molecule/deregister/side_effect.yml @@ -4,12 +4,18 @@ become: true vars: runner_name: "deregister-test-runner" + pre_tasks: + - name: Load role defaults + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults" tasks: - name: Ensure fake .runner file exists ansible.builtin.copy: - dest: "/var/lib/gitea-runner/{{ runner_name }}/.runner" + dest: "{{ gitea_runner_data_dir }}/.runner" content: | {"id": 1, "uuid": "test-uuid-1234", "name": "{{ runner_name }}", "address": "http://localhost:3000"} + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" mode: "0644" - name: Deregister runner @@ -19,7 +25,6 @@ runner_name: "deregister-test-runner" registration_token: "fake-token-for-testing" gitea_url: "http://localhost:3000" - runner_mode: "docker" skip_runner_registration: false tasks: - name: Include deregistration tasks diff --git a/ansible/roles/gitea-runner/molecule/deregister/verify.yml b/ansible/roles/gitea-runner/molecule/deregister/verify.yml index 7883cff..cfeec61 100644 --- a/ansible/roles/gitea-runner/molecule/deregister/verify.yml +++ b/ansible/roles/gitea-runner/molecule/deregister/verify.yml @@ -9,7 +9,7 @@ tasks: - name: Check registration file was removed ansible.builtin.stat: - path: "/var/lib/gitea-runner/deregister-test-runner/.runner" + path: "{{ gitea_runner_data_dir }}/.runner" register: runner_file_stat - name: Assert registration file no longer exists @@ -18,13 +18,13 @@ - not runner_file_stat.stat.exists fail_msg: "Registration file (.runner) was not removed by deregistration" - - name: Check systemd template still exists + - name: Check systemd user service still exists ansible.builtin.stat: - path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service" + path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service" register: service_stat - - name: Assert systemd template still exists after deregister + - name: Assert user service still exists after deregister ansible.builtin.assert: that: - service_stat.stat.exists - fail_msg: "Systemd template unit was incorrectly removed" + fail_msg: "Systemd user service was incorrectly removed" diff --git a/ansible/roles/gitea-runner/molecule/lifecycle/side_effect.yml b/ansible/roles/gitea-runner/molecule/lifecycle/side_effect.yml index 84d8fb2..67b2c45 100644 --- a/ansible/roles/gitea-runner/molecule/lifecycle/side_effect.yml +++ b/ansible/roles/gitea-runner/molecule/lifecycle/side_effect.yml @@ -5,24 +5,17 @@ vars: runner_name: "lifecycle-test-runner" pre_tasks: - - name: Check if systemd is available - ansible.builtin.stat: - path: /run/systemd/system - register: systemd_available + - name: Load role defaults + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults" tasks: - - name: Stop gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner-docker@{{ runner_name }}" - state: stopped - daemon_reload: true - when: systemd_available.stat.exists + - name: Stop gitea-runner user service + ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user stop gitea-runner" + changed_when: true - - name: Disable gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner-docker@{{ runner_name }}" - enabled: false - daemon_reload: true - when: systemd_available.stat.exists + - name: Disable gitea-runner user service + ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user disable gitea-runner" + changed_when: true - name: Re-enable and start runner hosts: all @@ -30,21 +23,14 @@ vars: runner_name: "lifecycle-test-runner" pre_tasks: - - name: Check if systemd is available - ansible.builtin.stat: - path: /run/systemd/system - register: systemd_available + - name: Load role defaults + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults" tasks: - - name: Enable gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner-docker@{{ runner_name }}" - enabled: true - daemon_reload: true - when: systemd_available.stat.exists + - name: Enable gitea-runner user service + ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user enable gitea-runner" + changed_when: true - - name: Start gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner-docker@{{ runner_name }}" - state: started - daemon_reload: true - when: systemd_available.stat.exists + - name: Start gitea-runner user service + ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user start gitea-runner" + changed_when: true diff --git a/ansible/roles/gitea-runner/molecule/lifecycle/verify.yml b/ansible/roles/gitea-runner/molecule/lifecycle/verify.yml index aa3e0ae..4e902c7 100644 --- a/ansible/roles/gitea-runner/molecule/lifecycle/verify.yml +++ b/ansible/roles/gitea-runner/molecule/lifecycle/verify.yml @@ -9,38 +9,20 @@ ansible.builtin.include_vars: dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults" tasks: - - name: Check systemd template unit exists + - name: Check systemd user service exists ansible.builtin.stat: - path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service" - register: template_stat + path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service" + register: service_stat - - name: Assert template unit exists + - name: Assert user service exists ansible.builtin.assert: that: - - template_stat.stat.exists - fail_msg: "Systemd template unit is missing" - - - name: Check if systemd is available - ansible.builtin.stat: - path: /run/systemd/system - register: systemd_available - - - name: Check instance is enabled - ansible.builtin.systemd: - name: "gitea-runner-{{ runner_mode | default('docker') }}@lifecycle-test-runner" - register: service_status - when: systemd_available.stat.exists - - - name: Assert instance is enabled - ansible.builtin.assert: - that: - - service_status.status.LoadState == 'loaded' - fail_msg: "Systemd instance is not loaded" - when: systemd_available.stat.exists + - service_stat.stat.exists + fail_msg: "Systemd user service is missing" - name: Check instance data directory exists after lifecycle ansible.builtin.stat: - path: "{{ gitea_runner_base_data_dir }}/{{ runner_name }}" + path: "{{ gitea_runner_data_dir }}" register: data_dir_stat - name: Assert instance data directory exists diff --git a/ansible/roles/gitea-runner/molecule/multi-instance/converge.yml b/ansible/roles/gitea-runner/molecule/multi-instance/converge.yml index f7ae281..ba72cc3 100644 --- a/ansible/roles/gitea-runner/molecule/multi-instance/converge.yml +++ b/ansible/roles/gitea-runner/molecule/multi-instance/converge.yml @@ -6,7 +6,6 @@ gitea_url: "http://localhost:3000" registration_token: "fake-token-for-testing" runner_name: "molecule-runner-a" - runner_mode: "binary" skip_runner_registration: true roles: - role: gitea-runner @@ -18,7 +17,6 @@ gitea_url: "http://localhost:3000" registration_token: "fake-token-for-testing" runner_name: "molecule-runner-b" - runner_mode: "binary" skip_runner_registration: true roles: - role: gitea-runner diff --git a/ansible/roles/gitea-runner/molecule/multi-instance/verify.yml b/ansible/roles/gitea-runner/molecule/multi-instance/verify.yml index e07e981..d103c90 100644 --- a/ansible/roles/gitea-runner/molecule/multi-instance/verify.yml +++ b/ansible/roles/gitea-runner/molecule/multi-instance/verify.yml @@ -6,24 +6,32 @@ - name: Load role defaults ansible.builtin.include_vars: dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults" - - name: Override runner mode for multi-instance scenario - ansible.builtin.set_fact: - runner_mode: "binary" tasks: - - name: Check systemd template unit exists + - name: Check first runner user exists ansible.builtin.stat: - path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service" - register: template_stat + path: "{{ gitea_runner_base_home }}/grm-molecule-runner-a" + register: home_a_stat - - name: Assert template unit exists + - name: Assert first runner user home exists ansible.builtin.assert: that: - - template_stat.stat.exists - fail_msg: "Systemd template unit is missing" + - home_a_stat.stat.exists + fail_msg: "First runner user home is missing" + + - name: Check second runner user exists + ansible.builtin.stat: + path: "{{ gitea_runner_base_home }}/grm-molecule-runner-b" + register: home_b_stat + + - name: Assert second runner user home exists + ansible.builtin.assert: + that: + - home_b_stat.stat.exists + fail_msg: "Second runner user home is missing" - name: Check first instance data directory exists ansible.builtin.stat: - path: "/var/lib/gitea-runner/molecule-runner-a" + path: "{{ gitea_runner_base_data_dir }}/molecule-runner-a" register: data_a_stat - name: Assert first instance data directory exists @@ -34,7 +42,7 @@ - name: Check second instance data directory exists ansible.builtin.stat: - path: "/var/lib/gitea-runner/molecule-runner-b" + path: "{{ gitea_runner_base_data_dir }}/molecule-runner-b" register: data_b_stat - name: Assert second instance data directory exists @@ -45,7 +53,7 @@ - name: Check first instance config exists ansible.builtin.stat: - path: "/etc/gitea-runner/molecule-runner-a/config.yaml" + path: "{{ gitea_runner_base_config_dir }}/molecule-runner-a/config.yaml" register: config_a_stat - name: Assert first instance config exists @@ -56,7 +64,7 @@ - name: Check second instance config exists ansible.builtin.stat: - path: "/etc/gitea-runner/molecule-runner-b/config.yaml" + path: "{{ gitea_runner_base_config_dir }}/molecule-runner-b/config.yaml" register: config_b_stat - name: Assert second instance config exists diff --git a/ansible/roles/gitea-runner/molecule/template-content/converge.yml b/ansible/roles/gitea-runner/molecule/template-content/converge.yml index 2b9106a..dbabef5 100644 --- a/ansible/roles/gitea-runner/molecule/template-content/converge.yml +++ b/ansible/roles/gitea-runner/molecule/template-content/converge.yml @@ -1,23 +1,11 @@ --- -- name: Converge Docker mode +- name: Converge hosts: all become: true vars: gitea_url: "http://localhost:3000" registration_token: "fake-token-for-testing" - runner_name: "template-docker-runner" - skip_runner_registration: true - roles: - - role: gitea-runner - -- name: Converge Binary mode - hosts: all - become: true - vars: - gitea_url: "http://localhost:3000" - registration_token: "fake-token-for-testing" - runner_name: "template-binary-runner" - runner_mode: "binary" + runner_name: "template-test-runner" skip_runner_registration: true roles: - role: gitea-runner diff --git a/ansible/roles/gitea-runner/molecule/template-content/verify.yml b/ansible/roles/gitea-runner/molecule/template-content/verify.yml index 034544c..3280642 100644 --- a/ansible/roles/gitea-runner/molecule/template-content/verify.yml +++ b/ansible/roles/gitea-runner/molecule/template-content/verify.yml @@ -2,65 +2,42 @@ - name: Verify hosts: all become: true + vars: + runner_name: "template-test-runner" pre_tasks: - name: Load role defaults ansible.builtin.include_vars: dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults" tasks: - - name: Check Docker mode systemd template exists + - name: Check systemd user service exists ansible.builtin.stat: - path: "/etc/systemd/system/gitea-runner-docker@.service" - register: docker_template_stat + path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service" + register: service_stat - - name: Assert Docker mode systemd template exists + - name: Assert user service exists ansible.builtin.assert: that: - - docker_template_stat.stat.exists - fail_msg: "Docker systemd template is missing" + - service_stat.stat.exists + fail_msg: "Systemd user service is missing" - - name: Read rendered Docker systemd template + - name: Read rendered user service template ansible.builtin.slurp: - src: "/etc/systemd/system/gitea-runner-docker@.service" - register: docker_template + src: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service" + register: service_template - - name: Assert Docker mode template contains expected directives + - name: Assert user service template contains expected directives ansible.builtin.assert: that: - - "'Type=oneshot' in docker_template.content | b64decode" - - "'RemainAfterExit=yes' in docker_template.content | b64decode" - - "'Requires=docker.service' in docker_template.content | b64decode" - - "'docker run -d' in docker_template.content | b64decode" - - "'ExecStop=/usr/bin/docker stop' in docker_template.content | b64decode" - - "'Restart=on-failure' in docker_template.content | b64decode" - fail_msg: "Docker systemd template is missing expected directives" - - - name: Check Binary mode systemd template exists - ansible.builtin.stat: - path: "/etc/systemd/system/gitea-runner-binary@.service" - register: binary_template_stat - - - name: Assert Binary mode systemd template exists - ansible.builtin.assert: - that: - - binary_template_stat.stat.exists - fail_msg: "Binary systemd template is missing" - - - name: Read rendered Binary systemd template - ansible.builtin.slurp: - src: "/etc/systemd/system/gitea-runner-binary@.service" - register: binary_template - - - name: Assert Binary mode template contains expected directives - ansible.builtin.assert: - that: - - "'Type=simple' in binary_template.content | b64decode" - - "'ExecStart={{ gitea_runner_binary_path }}' in binary_template.content | b64decode" - - "'Restart=on-failure' in binary_template.content | b64decode" - fail_msg: "Binary systemd template is missing expected directives" + - "'Type=simple' in service_template.content | b64decode" + - "'ExecStart={{ gitea_runner_binary_path }}' in service_template.content | b64decode" + - "'Restart=on-failure' in service_template.content | b64decode" + - "'DOCKER_HOST=unix:///run/user' in service_template.content | b64decode" + - "'XDG_RUNTIME_DIR=/run/user' in service_template.content | b64decode" + fail_msg: "User service template is missing expected directives" - name: Read rendered prune service template ansible.builtin.slurp: - src: "/etc/systemd/system/docker-prune.service" + src: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.service" register: prune_service - name: Assert prune service contains expected directives @@ -69,17 +46,16 @@ - "'Type=oneshot' in prune_service.content | b64decode" - "'docker system prune' in prune_service.content | b64decode" - "'docker volume prune' in prune_service.content | b64decode" - - "'label=gitea-runner=true' in prune_service.content | b64decode" fail_msg: "Prune service template is missing expected directives" - name: Read rendered prune timer template ansible.builtin.slurp: - src: "/etc/systemd/system/docker-prune.timer" + src: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer" register: prune_timer - name: Assert prune timer contains expected directives ansible.builtin.assert: that: - - "'OnCalendar=daily' in prune_timer.content | b64decode" + - "'OnCalendar={{ gitea_runner_prune_schedule }}' in prune_timer.content | b64decode" - "'Persistent=true' in prune_timer.content | b64decode" fail_msg: "Prune timer template is missing expected directives" diff --git a/ansible/roles/gitea-runner/molecule/update/converge.yml b/ansible/roles/gitea-runner/molecule/update/converge.yml index 815e51c..8eeb6f3 100644 --- a/ansible/roles/gitea-runner/molecule/update/converge.yml +++ b/ansible/roles/gitea-runner/molecule/update/converge.yml @@ -1,23 +1,11 @@ --- -- name: Converge Docker mode +- name: Converge hosts: all become: true vars: gitea_url: "http://localhost:3000" registration_token: "fake-token-for-testing" - runner_name: "update-docker-runner" - skip_runner_registration: true - roles: - - role: gitea-runner - -- name: Converge Binary mode - hosts: all - become: true - vars: - gitea_url: "http://localhost:3000" - registration_token: "fake-token-for-testing" - runner_name: "update-binary-runner" - runner_mode: "binary" + runner_name: "update-test-runner" skip_runner_registration: true roles: - role: gitea-runner diff --git a/ansible/roles/gitea-runner/molecule/update/side_effect.yml b/ansible/roles/gitea-runner/molecule/update/side_effect.yml index e1b44f9..59fa8b7 100644 --- a/ansible/roles/gitea-runner/molecule/update/side_effect.yml +++ b/ansible/roles/gitea-runner/molecule/update/side_effect.yml @@ -1,24 +1,11 @@ --- -- name: Update Docker mode runner +- name: Update runner hosts: all become: true vars: - runner_name: "update-docker-runner" - runner_mode: "docker" + runner_name: "update-test-runner" tasks: - - name: Include Docker update tasks + - name: Include update tasks ansible.builtin.include_role: name: gitea-runner - tasks_from: docker_update.yml - -- name: Update Binary mode runner - hosts: all - become: true - vars: - runner_name: "update-binary-runner" - runner_mode: "binary" - tasks: - - name: Include binary update tasks - ansible.builtin.include_role: - name: gitea-runner - tasks_from: binary_update.yml + tasks_from: update_runner.yml diff --git a/ansible/roles/gitea-runner/molecule/update/verify.yml b/ansible/roles/gitea-runner/molecule/update/verify.yml index ed5304b..25e15ee 100644 --- a/ansible/roles/gitea-runner/molecule/update/verify.yml +++ b/ansible/roles/gitea-runner/molecule/update/verify.yml @@ -2,23 +2,14 @@ - name: Verify hosts: all become: true + vars: + runner_name: "update-test-runner" pre_tasks: - name: Load role defaults ansible.builtin.include_vars: dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults" tasks: - - name: Check Docker runner image still exists after update - ansible.builtin.command: docker images gitea/runner --format '{{ '{{' }} .Repository {{ '}}' }}:{{ '{{' }} .Tag {{ '}}' }}' - register: image_list - changed_when: false - - - name: Assert runner image exists after update - ansible.builtin.assert: - that: - - "'gitea/runner' in image_list.stdout" - fail_msg: "Runner Docker image missing after update" - - - name: Check binary runner executable still exists after update + - name: Check runner binary still exists after update ansible.builtin.stat: path: "{{ gitea_runner_binary_path }}" register: binary_stat @@ -27,48 +18,26 @@ ansible.builtin.assert: that: - binary_stat.stat.exists - fail_msg: "Binary runner executable missing after update" + fail_msg: "Runner binary missing after update" - - name: Check Docker mode systemd template still exists + - name: Check systemd user service still exists ansible.builtin.stat: - path: "/etc/systemd/system/gitea-runner-docker@.service" - register: docker_service_stat + path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service" + register: service_stat - - name: Assert Docker mode systemd template still exists after update + - name: Assert user service exists after update ansible.builtin.assert: that: - - docker_service_stat.stat.exists - fail_msg: "Docker mode systemd template missing after update" + - service_stat.stat.exists + fail_msg: "Systemd user service missing after update" - - name: Check Binary mode systemd template still exists + - name: Check instance data directory still exists ansible.builtin.stat: - path: "/etc/systemd/system/gitea-runner-binary@.service" - register: binary_service_stat + path: "{{ gitea_runner_data_dir }}" + register: data_stat - - name: Assert Binary mode systemd template still exists after update + - name: Assert data directory exists after update ansible.builtin.assert: that: - - binary_service_stat.stat.exists - fail_msg: "Binary mode systemd template missing after update" - - - name: Check Docker mode data directory still exists - ansible.builtin.stat: - path: "/var/lib/gitea-runner/update-docker-runner" - register: docker_data_stat - - - name: Assert Docker mode data directory exists after update - ansible.builtin.assert: - that: - - docker_data_stat.stat.exists - fail_msg: "Docker runner data directory missing after update" - - - name: Check Binary mode data directory still exists - ansible.builtin.stat: - path: "/var/lib/gitea-runner/update-binary-runner" - register: binary_data_stat - - - name: Assert Binary mode data directory exists after update - ansible.builtin.assert: - that: - - binary_data_stat.stat.exists - fail_msg: "Binary runner data directory missing after update" + - data_stat.stat.exists + fail_msg: "Runner data directory missing after update" diff --git a/ansible/roles/gitea-runner/tasks/binary_update.yml b/ansible/roles/gitea-runner/tasks/binary_update.yml deleted file mode 100644 index db4adb6..0000000 --- a/ansible/roles/gitea-runner/tasks/binary_update.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- name: Include gitea_runner download - ansible.builtin.include_tasks: download_gitea_runner.yml - -- name: Restart gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner-{{ runner_mode }}@{{ runner_name }}" - state: restarted - daemon_reload: true - when: systemd_available is defined and systemd_available.stat is defined and systemd_available.stat.exists diff --git a/ansible/roles/gitea-runner/tasks/deregister.yml b/ansible/roles/gitea-runner/tasks/deregister.yml index ce8cecc..d846667 100644 --- a/ansible/roles/gitea-runner/tasks/deregister.yml +++ b/ansible/roles/gitea-runner/tasks/deregister.yml @@ -27,34 +27,18 @@ --no-interactive args: chdir: "{{ gitea_runner_data_dir }}" + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock" when: - runner_file_stat.stat.exists | default(false) | bool - not skip_runner_registration - - runner_mode == 'binary' register: deregister_output changed_when: deregister_output.rc == 0 failed_when: false -- name: Deregister runner with Gitea via Docker - ansible.builtin.command: > - docker run --rm - --entrypoint /usr/local/bin/gitea-runner - -v {{ gitea_runner_data_dir }}:/data - -w /data - {{ gitea_runner_docker_image }}:{{ gitea_runner_version | regex_replace('^v', '') }} - delete - --token {{ registration_token }} - --name {{ runner_name }} - --instance {{ gitea_url }} - --no-interactive - when: - - runner_file_stat.stat.exists | default(false) | bool - - not skip_runner_registration - - runner_mode == 'docker' - register: deregister_docker_output - changed_when: deregister_docker_output.rc == 0 - failed_when: false - - name: Remove runner registration file ansible.builtin.file: path: "{{ gitea_runner_data_dir }}/.runner" diff --git a/ansible/roles/gitea-runner/tasks/docker.yml b/ansible/roles/gitea-runner/tasks/docker.yml deleted file mode 100644 index b586645..0000000 --- a/ansible/roles/gitea-runner/tasks/docker.yml +++ /dev/null @@ -1,100 +0,0 @@ ---- -- name: Check if Docker is already installed - ansible.builtin.command: docker --version - register: docker_check - changed_when: false - failed_when: false - -- name: Install Docker (Debian/Ubuntu) - when: - - ansible_facts['os_family'] == 'Debian' - - docker_check.rc != 0 - block: - - name: Remove old Docker apt repository files - ansible.builtin.file: - path: "{{ item }}" - state: absent - loop: - - /etc/apt/sources.list.d/docker.list - - /etc/apt/keyrings/docker.gpg - - - name: Install prerequisite packages - ansible.builtin.apt: - name: - - apt-transport-https - - ca-certificates - - curl - - gnupg - state: present - update_cache: true - cache_valid_time: 0 - - - name: Add Docker GPG key - ansible.builtin.get_url: - url: https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg - dest: "{{ docker_gpg_key_path }}" - mode: "0644" - - - name: Add Docker repository - ansible.builtin.deb822_repository: - name: docker - types: deb - uris: https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }} - suites: "{{ ansible_facts['distribution_release'] }}" - components: stable - signed_by: "{{ docker_gpg_key_path }}" - state: present - - - name: Install Docker packages - ansible.builtin.apt: - name: - - docker-ce - - docker-ce-cli - - containerd.io - - docker-compose-plugin - state: present - update_cache: true - cache_valid_time: 0 - -- name: Install Docker (Arch Linux) - when: - - ansible_facts['os_family'] == 'Archlinux' - - docker_check.rc != 0 - block: - - name: Install Docker packages - community.general.pacman: - name: - - docker - - docker-compose - state: present - update_cache: true - -- name: Ensure Docker service is running (systemd) - ansible.builtin.systemd: - name: docker - state: started - enabled: true - when: systemd_available.stat.exists - -- name: Start Docker daemon manually (no systemd) - ansible.builtin.shell: nohup dockerd > /var/log/dockerd.log 2>&1 & - when: - - not systemd_available.stat.exists - - docker_check.rc != 0 - changed_when: true - -- name: Wait for Docker daemon to be ready - ansible.builtin.command: docker version - register: docker_ready - until: docker_ready.rc == 0 - retries: 10 - delay: 2 - changed_when: false - when: not systemd_available.stat.exists - -- name: Add user to docker group - ansible.builtin.user: - name: "{{ ansible_user | default(ansible_facts['user_id']) }}" - groups: docker - append: true - when: ansible_user is defined or ansible_facts['user_id'] is defined diff --git a/ansible/roles/gitea-runner/tasks/docker_mode.yml b/ansible/roles/gitea-runner/tasks/docker_mode.yml deleted file mode 100644 index 32fee6a..0000000 --- a/ansible/roles/gitea-runner/tasks/docker_mode.yml +++ /dev/null @@ -1,55 +0,0 @@ ---- -- name: Include Docker installation - ansible.builtin.include_tasks: docker.yml - -- name: Ensure runner data directory exists - ansible.builtin.file: - path: "{{ gitea_runner_data_dir }}" - state: directory - mode: "0755" - -- name: Create gitea_runner config file in data directory - ansible.builtin.template: - src: gitea-runner-config.yaml.j2 - dest: "{{ gitea_runner_data_dir }}/config.yaml" - mode: "0644" - -- name: Pull gitea_runner image - ansible.builtin.command: docker pull {{ gitea_runner_docker_image }}:{{ gitea_runner_version | regex_replace('^v', '') }} - changed_when: "'Downloaded newer image' in pull_result.stdout or 'Status: Downloaded' in pull_result.stdout" - register: pull_result - -- name: Check if runner is already registered - ansible.builtin.stat: - path: "{{ gitea_runner_data_dir }}/.runner" - register: runner_registered - -- name: Register runner with Gitea via Docker - ansible.builtin.command: > - docker run --rm - --entrypoint /usr/local/bin/gitea-runner - -v {{ gitea_runner_data_dir }}:/data - -w /data - {{ gitea_runner_docker_image }}:{{ gitea_runner_version | regex_replace('^v', '') }} - register - --token {{ registration_token }} - --name {{ runner_name }} - --instance {{ gitea_url }} - --labels {{ runner_labels }} - --no-interactive - when: - - not runner_registered.stat.exists - - not skip_runner_registration - register: register_output - changed_when: "'already exists' not in register_output.stdout | default('')" - timeout: 60 - -- name: Restart gitea-runner service after registration - ansible.builtin.systemd: - name: "gitea-runner-docker@{{ runner_name }}" - state: restarted - daemon_reload: true - when: systemd_available.stat.exists | default(false) | bool - -- name: Include service setup - ansible.builtin.include_tasks: service.yml diff --git a/ansible/roles/gitea-runner/tasks/docker_update.yml b/ansible/roles/gitea-runner/tasks/docker_update.yml deleted file mode 100644 index 1f6f4e6..0000000 --- a/ansible/roles/gitea-runner/tasks/docker_update.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -- name: Pull gitea_runner image - ansible.builtin.command: docker pull {{ gitea_runner_docker_image }}:{{ gitea_runner_version | regex_replace('^v', '') }} - changed_when: "'Downloaded newer image' in pull_result.stdout or 'Status: Downloaded' in pull_result.stdout" - register: pull_result - -- name: Restart gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner-{{ runner_mode }}@{{ runner_name }}" - state: restarted - daemon_reload: true - when: systemd_available is defined and systemd_available.stat is defined and systemd_available.stat.exists diff --git a/ansible/roles/gitea-runner/tasks/binary_mode.yml b/ansible/roles/gitea-runner/tasks/install_runner.yml similarity index 64% rename from ansible/roles/gitea-runner/tasks/binary_mode.yml rename to ansible/roles/gitea-runner/tasks/install_runner.yml index 083ca38..e5190bc 100644 --- a/ansible/roles/gitea-runner/tasks/binary_mode.yml +++ b/ansible/roles/gitea-runner/tasks/install_runner.yml @@ -1,18 +1,14 @@ --- -- name: Include Docker installation - ansible.builtin.include_tasks: docker.yml - - name: Include gitea_runner download ansible.builtin.include_tasks: download_gitea_runner.yml -- name: Ensure runner data directory exists - ansible.builtin.file: - path: "{{ gitea_runner_data_dir }}" - state: directory - mode: "0755" - -- name: Include config creation - ansible.builtin.include_tasks: config.yml +- name: Create gitea_runner config file + ansible.builtin.template: + src: gitea-runner-config.yaml.j2 + dest: "{{ gitea_runner_config_dir }}/config.yaml" + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" + mode: "0644" - name: Include validation ansible.builtin.include_tasks: validate.yml diff --git a/ansible/roles/gitea-runner/tasks/integration_test.yml b/ansible/roles/gitea-runner/tasks/integration_test.yml index f381ba6..415bfca 100644 --- a/ansible/roles/gitea-runner/tasks/integration_test.yml +++ b/ansible/roles/gitea-runner/tasks/integration_test.yml @@ -18,19 +18,14 @@ else {} }} when: runner_file_stat.stat.exists | default(false) | bool -- name: Verify runner container running (Docker mode) - ansible.builtin.shell: | - docker ps --filter "name=gitea-runner-{{ runner_name }}" --format "{{ '{{.Status}}' }}" - register: docker_check - changed_when: false - when: runner_mode == 'docker' - -- name: Verify runner service active (Binary mode) - ansible.builtin.systemd: - name: "gitea-runner-{{ runner_mode }}@{{ runner_name }}" - state: started +- name: Verify runner user service active + ansible.builtin.command: systemctl --user is-active gitea-runner + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" register: service_check - when: runner_mode == 'binary' + changed_when: false - name: Validate runner installation ansible.builtin.fail: @@ -39,16 +34,12 @@ {% if not (runner_file_stat.stat.exists | default(false)) %} - Registration file (.runner) is missing. Registration may have failed. {% endif %} - {% if runner_mode == 'docker' and not (docker_check.stdout | default('')) %} - - Docker container is not running. - {% endif %} - {% if runner_mode == 'binary' and not (service_check.status.ActiveState | default('')) == 'active' %} - - Systemd service is not active. + {% if not (service_check.stdout | default('') | trim) == 'active' %} + - Systemd user service is not active. {% endif %} when: > not (runner_file_stat.stat.exists | default(false)) - or (runner_mode == 'docker' and not (docker_check.stdout | default(''))) - or (runner_mode == 'binary' and not (service_check.status.ActiveState | default('')) == 'active') + or not (service_check.stdout | default('') | trim) == 'active' - name: Report runner status ansible.builtin.debug: @@ -58,8 +49,7 @@ {% if runner_reg.id is defined %}Runner ID: {{ runner_reg.id }}{% endif %} {% if runner_reg.uuid is defined %}UUID: {{ runner_reg.uuid }}{% endif %} {% if runner_reg.address is defined %}Gitea: {{ runner_reg.address }}{% endif %} - {% if runner_mode == 'docker' %}Container: {{ docker_check.stdout | default('unknown') }}{% endif %} - {% if runner_mode == 'binary' %}Service: {{ service_check.status.ActiveState | default('unknown') }}{% endif %} + Service: {{ service_check.stdout | default('unknown') | trim }} - name: Optional Gitea API verification when: diff --git a/ansible/roles/gitea-runner/tasks/main.yml b/ansible/roles/gitea-runner/tasks/main.yml index 8774a57..9bff087 100644 --- a/ansible/roles/gitea-runner/tasks/main.yml +++ b/ansible/roles/gitea-runner/tasks/main.yml @@ -2,13 +2,14 @@ - name: Include systemd availability check ansible.builtin.include_tasks: systemd_check.yml -- name: Install runner (Docker mode) - ansible.builtin.include_tasks: docker_mode.yml - when: runner_mode == 'docker' +- name: Include user setup + ansible.builtin.include_tasks: user_setup.yml -- name: Install runner (Binary mode) - ansible.builtin.include_tasks: binary_mode.yml - when: runner_mode == 'binary' +- name: Include rootless Docker setup + ansible.builtin.include_tasks: rootless_docker.yml + +- name: Include runner install + ansible.builtin.include_tasks: install_runner.yml - name: Include prune setup ansible.builtin.include_tasks: prune.yml diff --git a/ansible/roles/gitea-runner/tasks/prune.yml b/ansible/roles/gitea-runner/tasks/prune.yml index a80c54b..35ed227 100644 --- a/ansible/roles/gitea-runner/tasks/prune.yml +++ b/ansible/roles/gitea-runner/tasks/prune.yml @@ -1,20 +1,33 @@ --- -- name: Create docker-prune service file +- name: Create docker-prune user service file ansible.builtin.template: src: docker-prune.service.j2 - dest: /etc/systemd/system/docker-prune.service + dest: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.service" + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" mode: "0644" -- name: Create docker-prune timer file +- name: Create docker-prune user timer file ansible.builtin.template: src: docker-prune.timer.j2 - dest: /etc/systemd/system/docker-prune.timer + dest: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer" + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" mode: "0644" -- name: Enable and start docker-prune timer - ansible.builtin.systemd: - name: docker-prune.timer - state: started - enabled: true - daemon_reload: true +- name: Reload systemd user daemon for prune timer + ansible.builtin.command: systemctl --user daemon-reload + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + changed_when: true + +- name: Enable and start docker-prune user timer + ansible.builtin.command: systemctl --user enable --now docker-prune.timer + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + changed_when: true when: systemd_available.stat.exists diff --git a/ansible/roles/gitea-runner/tasks/register.yml b/ansible/roles/gitea-runner/tasks/register.yml index f05f127..427549e 100644 --- a/ansible/roles/gitea-runner/tasks/register.yml +++ b/ansible/roles/gitea-runner/tasks/register.yml @@ -3,6 +3,8 @@ ansible.builtin.file: path: "{{ gitea_runner_data_dir }}" state: directory + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" mode: "0755" - name: Check if runner is already registered @@ -12,7 +14,7 @@ - name: Register runner with Gitea ansible.builtin.command: > - "{{ gitea_runner_binary_path }}" register + {{ gitea_runner_binary_path }} register --token {{ registration_token }} --name {{ runner_name }} --instance {{ gitea_url }} @@ -20,14 +22,12 @@ --no-interactive args: chdir: "{{ gitea_runner_data_dir }}" + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock" when: not runner_registered.stat.exists register: register_output changed_when: "'already exists' not in register_output.stdout | default('')" timeout: 60 - -- name: Restart gitea-runner service after registration - ansible.builtin.systemd: - name: "gitea-runner-binary@{{ runner_name }}" - state: restarted - daemon_reload: true - when: systemd_available.stat.exists | default(false) | bool diff --git a/ansible/roles/gitea-runner/tasks/rootless_docker.yml b/ansible/roles/gitea-runner/tasks/rootless_docker.yml new file mode 100644 index 0000000..4057cfb --- /dev/null +++ b/ansible/roles/gitea-runner/tasks/rootless_docker.yml @@ -0,0 +1,73 @@ +--- +- name: Install rootless Docker dependencies (Debian/Ubuntu) + ansible.builtin.apt: + name: + - uidmap + - slirp4netns + - fuse-overlayfs + - docker-ce + - docker-ce-cli + - docker-ce-rootless-extras + - containerd.io + - docker-compose-plugin + - rsync + state: present + update_cache: true + cache_valid_time: 0 + when: ansible_facts['os_family'] == 'Debian' + +- name: Install rootless Docker dependencies (Arch Linux) + community.general.pacman: + name: + - docker + - docker-compose + - slirp4netns + - fuse-overlayfs + - rsync + state: present + update_cache: true + when: ansible_facts['os_family'] == 'Archlinux' + +- name: Check if rootless Docker is already set up + ansible.builtin.stat: + path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service" + register: rootless_docker_check + +- name: Set up rootless Docker for runner user + ansible.builtin.command: dockerd-rootless-setuptool.sh install + args: + creates: "{{ gitea_runner_home }}/.config/systemd/user/docker.service" + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + when: not rootless_docker_check.stat.exists + +- name: Start rootless Docker daemon (systemd user service) + ansible.builtin.command: systemctl --user start docker + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + changed_when: true + +- name: Enable rootless Docker daemon (systemd user service) + ansible.builtin.command: systemctl --user enable docker + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + changed_when: true + +- name: Wait for rootless Docker daemon to be ready + ansible.builtin.command: docker version + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock" + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + register: docker_ready + until: docker_ready.rc == 0 + retries: 10 + delay: 2 + changed_when: false diff --git a/ansible/roles/gitea-runner/tasks/service.yml b/ansible/roles/gitea-runner/tasks/service.yml index 03582a7..f1fa425 100644 --- a/ansible/roles/gitea-runner/tasks/service.yml +++ b/ansible/roles/gitea-runner/tasks/service.yml @@ -1,17 +1,25 @@ --- -- name: Create systemd template service file +- name: Create systemd user service file ansible.builtin.template: - src: gitea-runner@.service.j2 - dest: "/etc/systemd/system/gitea-runner-{{ runner_mode }}@.service" + src: gitea-runner-user.service.j2 + dest: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service" + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" mode: "0644" - notify: - - Reload systemd - - Restart gitea-runner -- name: Enable and start gitea-runner instance - ansible.builtin.systemd: - name: "gitea-runner-{{ runner_mode }}@{{ runner_name }}" - state: started - enabled: true - daemon_reload: true +- name: Reload systemd user daemon + ansible.builtin.command: systemctl --user daemon-reload + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + changed_when: true + +- name: Enable and start gitea-runner user service + ansible.builtin.command: systemctl --user enable --now gitea-runner + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + changed_when: true when: systemd_available.stat.exists diff --git a/ansible/roles/gitea-runner/tasks/update_runner.yml b/ansible/roles/gitea-runner/tasks/update_runner.yml new file mode 100644 index 0000000..fbb555c --- /dev/null +++ b/ansible/roles/gitea-runner/tasks/update_runner.yml @@ -0,0 +1,12 @@ +--- +- name: Include gitea_runner download + ansible.builtin.include_tasks: download_gitea_runner.yml + +- name: Restart gitea-runner user service + ansible.builtin.command: systemctl --user restart gitea-runner + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + when: systemd_available.stat.exists | default(false) | bool + changed_when: true diff --git a/ansible/roles/gitea-runner/tasks/user_setup.yml b/ansible/roles/gitea-runner/tasks/user_setup.yml new file mode 100644 index 0000000..afac28b --- /dev/null +++ b/ansible/roles/gitea-runner/tasks/user_setup.yml @@ -0,0 +1,65 @@ +--- +- name: Create per-runner system user + ansible.builtin.user: + name: "{{ gitea_runner_service_user }}" + home: "{{ gitea_runner_home }}" + shell: /bin/bash + system: true + create_home: true + register: runner_user + +- name: Set runner UID fact + ansible.builtin.set_fact: + gitea_runner_uid: "{{ runner_user.uid }}" + +- name: Enable lingering for runner user + ansible.builtin.command: loginctl enable-linger {{ gitea_runner_service_user }} + changed_when: true + +- name: Ensure subuid entry for runner user + ansible.builtin.lineinfile: + path: /etc/subuid + regexp: "^{{ gitea_runner_service_user }}:" + line: "{{ gitea_runner_service_user }}:100000:65536" + create: true + mode: "0644" + +- name: Ensure subgid entry for runner user + ansible.builtin.lineinfile: + path: /etc/subgid + regexp: "^{{ gitea_runner_service_user }}:" + line: "{{ gitea_runner_service_user }}:100000:65536" + create: true + mode: "0644" + +- name: Ensure XDG_RUNTIME_DIR exists + ansible.builtin.file: + path: "/run/user/{{ gitea_runner_uid }}" + state: directory + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" + mode: "0700" + +- name: Ensure runner data directory exists + ansible.builtin.file: + path: "{{ gitea_runner_data_dir }}" + state: directory + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" + mode: "0755" + +- name: Ensure runner config directory exists + ansible.builtin.file: + path: "{{ gitea_runner_config_dir }}" + state: directory + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" + mode: "0755" + +- name: Ensure systemd user directory exists + ansible.builtin.file: + path: "{{ gitea_runner_home }}/.config/systemd/user" + state: directory + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" + mode: "0755" diff --git a/ansible/roles/gitea-runner/tasks/validate.yml b/ansible/roles/gitea-runner/tasks/validate.yml index 564ea0f..6e43c81 100644 --- a/ansible/roles/gitea-runner/tasks/validate.yml +++ b/ansible/roles/gitea-runner/tasks/validate.yml @@ -14,7 +14,12 @@ register: gitea_runner_version_output changed_when: false -- name: Verify Docker connectivity +- name: Verify rootless Docker connectivity ansible.builtin.command: docker version + become: true + become_user: "{{ gitea_runner_service_user }}" + environment: + DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock" + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" register: docker_version_output changed_when: false diff --git a/ansible/roles/gitea-runner/templates/docker-prune.service.j2 b/ansible/roles/gitea-runner/templates/docker-prune.service.j2 index 20f1744..2418563 100644 --- a/ansible/roles/gitea-runner/templates/docker-prune.service.j2 +++ b/ansible/roles/gitea-runner/templates/docker-prune.service.j2 @@ -1,9 +1,9 @@ [Unit] Description=Docker prune for Gitea runner resources -After=docker.service -Requires=docker.service [Service] Type=oneshot +Environment=DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/docker.sock +Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }} ExecStart=/usr/bin/docker system prune -f --filter "label={{ gitea_runner_prune_label }}" --filter "until={{ gitea_runner_prune_until }}" ExecStart=/usr/bin/docker volume prune -f --filter "label={{ gitea_runner_prune_label }}" --filter "until={{ gitea_runner_prune_until }}" diff --git a/ansible/roles/gitea-runner/templates/gitea-runner-config.yaml.j2 b/ansible/roles/gitea-runner/templates/gitea-runner-config.yaml.j2 index 0de733e..a5adca5 100644 --- a/ansible/roles/gitea-runner/templates/gitea-runner-config.yaml.j2 +++ b/ansible/roles/gitea-runner/templates/gitea-runner-config.yaml.j2 @@ -8,3 +8,4 @@ runner: container: label: "{{ gitea_runner_container_label }}" + docker_host: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock" diff --git a/ansible/roles/gitea-runner/templates/gitea-runner-user.service.j2 b/ansible/roles/gitea-runner/templates/gitea-runner-user.service.j2 new file mode 100644 index 0000000..08830de --- /dev/null +++ b/ansible/roles/gitea-runner/templates/gitea-runner-user.service.j2 @@ -0,0 +1,17 @@ +[Unit] +Description=Gitea Actions Runner (rootless) +After=docker.service + +[Service] +Type=simple +ExecStart={{ gitea_runner_binary_path }} daemon --config {{ gitea_runner_config_dir }}/config.yaml +WorkingDirectory={{ gitea_runner_data_dir }} +Environment=DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/docker.sock +Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }} +ExecStop=/bin/kill -TERM $MAINPID +TimeoutStopSec=30 +Restart=on-failure +RestartSec={{ gitea_runner_service_restart_sec }} + +[Install] +WantedBy=default.target diff --git a/ansible/roles/gitea-runner/templates/gitea-runner@.service.j2 b/ansible/roles/gitea-runner/templates/gitea-runner@.service.j2 deleted file mode 100644 index 39805b5..0000000 --- a/ansible/roles/gitea-runner/templates/gitea-runner@.service.j2 +++ /dev/null @@ -1,35 +0,0 @@ -[Unit] -Description=Gitea Actions Runner %i ({% if runner_mode == 'docker' %}docker{% else %}binary{% endif %}) -After=network.target{% if runner_mode == 'docker' %} docker.service -Requires=docker.service{% endif %} - -[Service] -{% if runner_mode == 'docker' %} -Type=oneshot -RemainAfterExit=yes -ExecStartPre=-/usr/bin/docker rm -f gitea-runner-%i -ExecStart=/usr/bin/docker run -d --name gitea-runner-%i \ - --restart=no \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v {{ gitea_runner_base_data_dir }}/%i:/data \ - -w /data \ - {{ gitea_runner_docker_image }}:{{ gitea_runner_version | regex_replace('^v', '') }} \ - daemon --config /data/config.yaml -ExecStop=/usr/bin/docker stop -t 30 gitea-runner-%i -ExecStopPost=-/usr/bin/docker rm -f gitea-runner-%i -Restart=on-failure -RestartSec={{ gitea_runner_service_restart_sec }} -{% else %} -Type=simple -ExecStart={{ gitea_runner_binary_path }} daemon --config {{ gitea_runner_base_config_dir }}/%i/config.yaml -WorkingDirectory={{ gitea_runner_base_data_dir }}/%i -ExecStop=/bin/kill -TERM $MAINPID -TimeoutStopSec=30 -Restart=on-failure -RestartSec={{ gitea_runner_service_restart_sec }} -User={{ gitea_runner_service_user }} -Group=docker -{% endif %} - -[Install] -WantedBy=multi-user.target diff --git a/ansible/start-runner.yml b/ansible/start-runner.yml index b879707..3931025 100644 --- a/ansible/start-runner.yml +++ b/ansible/start-runner.yml @@ -22,9 +22,11 @@ - not runner_registered.stat.exists - not skip_runner_registration | default(false) - - name: Start gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner@{{ runner_name }}" - state: started - daemon_reload: true + - name: Start gitea-runner user service + ansible.builtin.command: systemctl --user start gitea-runner + become: true + become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" when: systemd_available.stat.exists + changed_when: true diff --git a/ansible/status-runner.yml b/ansible/status-runner.yml index 64c4145..5580f25 100644 --- a/ansible/status-runner.yml +++ b/ansible/status-runner.yml @@ -9,19 +9,19 @@ name: gitea-runner tasks_from: systemd_check.yml - - name: Check systemd status - ansible.builtin.systemd: - name: "gitea-runner@{{ runner_name }}" + - name: Check systemd user service status + ansible.builtin.command: systemctl --user is-active gitea-runner + become: true + become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" register: service_status + changed_when: false when: systemd_available.stat.exists - - name: Report systemd status + - name: Report service status ansible.builtin.debug: - msg: > - Service gitea-runner@{{ runner_name }}: - ActiveState={{ service_status.status.ActiveState | default('unknown') }}, - SubState={{ service_status.status.SubState | default('unknown') }}, - LoadState={{ service_status.status.LoadState | default('unknown') }} + msg: "Service gitea-runner: {{ service_status.stdout | default('unknown') | trim }}" when: systemd_available.stat.exists - name: Check runner registration file @@ -32,15 +32,3 @@ - name: Report runner registration ansible.builtin.debug: msg: "Runner registration file exists: {{ runner_file_stat.stat.exists | default(false) }}" - - - name: Check container status (Docker mode) - ansible.builtin.shell: | - docker ps --filter "name=gitea-runner-{{ runner_name }}" --format "{{ '{{.Status}}' }}" - register: docker_check - changed_when: false - when: runner_mode | default('docker') == 'docker' - - - name: Report container status - ansible.builtin.debug: - msg: "Container status: {{ docker_check.stdout | default('not running') }}" - when: runner_mode | default('docker') == 'docker' diff --git a/ansible/stop-runner.yml b/ansible/stop-runner.yml index e537107..b136575 100644 --- a/ansible/stop-runner.yml +++ b/ansible/stop-runner.yml @@ -9,9 +9,11 @@ name: gitea-runner tasks_from: systemd_check.yml - - name: Stop gitea-runner systemd instance - ansible.builtin.systemd: - name: "gitea-runner@{{ runner_name }}" - state: stopped - daemon_reload: true + - name: Stop gitea-runner user service + ansible.builtin.command: systemctl --user stop gitea-runner + become: true + become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" when: systemd_available.stat.exists + changed_when: true diff --git a/ansible/update-runner.yml b/ansible/update-runner.yml index 6dfcaa5..252be93 100644 --- a/ansible/update-runner.yml +++ b/ansible/update-runner.yml @@ -4,14 +4,7 @@ become: true vars: {} tasks: - - name: Update runner (Docker mode) + - name: Update runner ansible.builtin.include_role: name: gitea-runner - tasks_from: docker_update.yml - when: runner_mode | default('docker') == 'docker' - - - name: Update runner (Binary mode) - ansible.builtin.include_role: - name: gitea-runner - tasks_from: binary_update.yml - when: runner_mode | default('docker') == 'binary' + tasks_from: update_runner.yml diff --git a/scripts/auto_merge.py b/scripts/auto_merge.py index fa6c44a..2d2b1b9 100644 --- a/scripts/auto_merge.py +++ b/scripts/auto_merge.py @@ -1,11 +1,14 @@ #!/usr/bin/env python3 """Auto-merge PR by extracting task ID from branch and validating PR title. +Waits for CI checks to complete before attempting the merge. + Usage: REPO_TOKEN= python3 scripts/auto_merge.py [label_name] """ import os +import time import click from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType] @@ -16,6 +19,8 @@ from gitea_runner_manager.exceptions import APIError from gitea_runner_manager.i18n import _ READY_TO_MERGE = "ready-to-merge" +MAX_WAIT_SECONDS = 900 # 15 minutes +POLL_INTERVAL_SECONDS = 30 load_dotenv(override=True) @@ -45,6 +50,66 @@ def has_ready_to_merge_label(client: GiteaClient, pr_number: str) -> bool: return any(label.get("name") == READY_TO_MERGE for label in labels) +def wait_for_ci( + client: GiteaClient, sha: str, max_wait: int = MAX_WAIT_SECONDS, poll_interval: int = POLL_INTERVAL_SECONDS +) -> bool: + """Poll commit statuses until all CI checks are complete (not pending). + + Returns True if all checks are successful, False if any failed or timed out. + """ + elapsed = 0 + while elapsed < max_wait: + statuses = client.get_commit_status(sha) + if not statuses: + click.echo(_("No CI checks reported yet, waiting...")) + time.sleep(poll_interval) + elapsed += poll_interval + continue + + # Deduplicate by context — keep the latest status per context. + latest: dict[str, dict[str, object]] = {} + for s in statuses: + ctx = s.get("context", "") + if ctx not in latest or s.get("updated_at", "") > latest[ctx].get("updated_at", ""): + latest[ctx] = s + + ci_statuses = {ctx: s for ctx, s in latest.items() if ctx.startswith("CI /")} + if not ci_statuses: + click.echo(_("No CI checks found yet, waiting...")) + time.sleep(poll_interval) + elapsed += poll_interval + continue + + pending = [ctx for ctx, s in ci_statuses.items() if s.get("status") in ("pending", "waiting")] + if not pending: + # All CI checks are complete — check if they all succeeded. + failed = [ + ctx + for ctx, s in ci_statuses.items() + if s.get("status") not in ("success", "ok") + ] + if failed: + click.echo( + _("CI checks failed: {failed}", failed=", ".join(sorted(failed))) + ) + return False + click.echo(_("All CI checks passed.")) + return True + + click.echo( + _( + "Waiting for CI checks: {pending} ({elapsed}s elapsed)", + pending=", ".join(sorted(pending)), + elapsed=elapsed, + ) + ) + time.sleep(poll_interval) + elapsed += poll_interval + + click.echo(_("Timed out waiting for CI checks after {max_wait}s.", max_wait=max_wait)) + return False + + @click.command() @click.argument("branch") @click.argument("pr_title") @@ -75,6 +140,18 @@ def main(branch: str, pr_title: str, repo: str, pr_number: str, label_name: str) validate_pr_title(pr_title) + # Wait for CI checks to complete before attempting merge. + pr = client.get_pr(pr_number) + sha = pr.get("head", {}).get("sha", "") + if sha: + click.echo(_("Waiting for CI checks on commit {sha}...", sha=sha[:8])) + if not wait_for_ci(client, sha): + raise click.ClickException( + _("Cannot merge: CI checks did not pass. Please fix failing checks and re-label.") + ) + else: + click.echo(_("Warning: could not determine PR head SHA, proceeding without CI wait.")) + merge_title = f"{task_id}: {pr_title}" try: diff --git a/scripts/check_test_speed.py b/scripts/check_test_speed.py index 1123cfe..455fbb5 100644 --- a/scripts/check_test_speed.py +++ b/scripts/check_test_speed.py @@ -7,10 +7,8 @@ Usage: from __future__ import annotations -import argparse import re import subprocess # nosec B404 -import sys import click diff --git a/scripts/distribute_molecule.py b/scripts/distribute_molecule.py index 330e885..570073a 100644 --- a/scripts/distribute_molecule.py +++ b/scripts/distribute_molecule.py @@ -1,19 +1,25 @@ #!/usr/bin/env python3 -"""Distribute molecule scenarios across N parallel runners. +"""Distribute molecule (scenario, platform) pairs across N parallel runners. Discovers all molecule scenarios under ansible/roles/*/molecule/ and -splits them evenly across the requested number of runners. +crosses them with the supported OS platform matrix, then splits the +resulting test pairs evenly across the requested number of runners. + +Each pair is printed as ``scenario|platform_name|platform_image|platform_command`` +so the CI workflow can set the appropriate environment variables. Usage: python3 scripts/distribute_molecule.py --runner-index 0 --max-runners 3 - # prints: default deregister + # prints: default|ubuntu-2204|ubuntu:22.04| lifecycle|ubuntu-2204|ubuntu:22.04| ... python3 scripts/distribute_molecule.py --list # prints all scenarios, one per line + python3 scripts/distribute_molecule.py --list-platforms + # prints all platforms, one per line """ from __future__ import annotations -import sys +from dataclasses import dataclass from pathlib import Path import click @@ -23,6 +29,36 @@ from gitea_runner_manager.i18n import _ DEFAULT_MAX_RUNNERS = 3 MOLECULE_ROOT = Path("ansible/roles/gitea-runner/molecule") +#: Supported OS platform matrix. +#: Each entry maps a short name to (image, command). +PLATFORMS: list[dict[str, str]] = [ + {"name": "ubuntu-2204", "image": "geerlingguy/docker-ubuntu2204-ansible:latest", "command": ""}, + {"name": "ubuntu-2404", "image": "geerlingguy/docker-ubuntu2404-ansible:latest", "command": ""}, + {"name": "debian-12", "image": "geerlingguy/docker-debian12-ansible:latest", "command": ""}, + {"name": "archlinux", "image": "marcstraube/archlinux-ansible:latest", "command": "/usr/lib/systemd/systemd"}, +] + + +@dataclass(frozen=True) +class TestPair: + """A (scenario, platform) combination to test.""" + + scenario: str + platform: dict[str, str] + + def encode(self) -> str: + """Serialize to a pipe-delimited string for CI consumption.""" + return f"{self.scenario}|{self.platform['name']}|{self.platform['image']}|{self.platform['command']}" + + @staticmethod + def decode(encoded: str) -> TestPair: + """Deserialize from a pipe-delimited string.""" + parts = encoded.split("|") + return TestPair( + scenario=parts[0], + platform={"name": parts[1], "image": parts[2], "command": parts[3]}, + ) + def discover_scenarios(root: Path | None = None) -> list[str]: """Return sorted list of molecule scenario directory names.""" @@ -40,19 +76,26 @@ def discover_scenarios(root: Path | None = None) -> list[str]: return sorted(scenarios) -def distribute(scenarios: list[str], max_runners: int) -> list[list[str]]: - """Split *scenarios* into *max_runners* balanced groups (round-robin).""" - groups: list[list[str]] = [[] for _ in range(max_runners)] - for i, scenario in enumerate(scenarios): - groups[i % max_runners].append(scenario) +def build_pairs(scenarios: list[str], platforms: list[dict[str, str]] | None = None) -> list[TestPair]: + """Build the full cross-product of scenarios and platforms.""" + if platforms is None: + platforms = PLATFORMS + return [TestPair(s, p) for s in scenarios for p in platforms] + + +def distribute(pairs: list[TestPair], max_runners: int) -> list[list[TestPair]]: + """Split *pairs* into *max_runners* balanced groups (round-robin).""" + groups: list[list[TestPair]] = [[] for _ in range(max_runners)] + for i, pair in enumerate(pairs): + groups[i % max_runners].append(pair) return groups -def scenarios_for_runner( - scenarios: list[str], runner_index: int, max_runners: int -) -> list[str]: - """Return the subset of scenarios assigned to *runner_index*.""" - groups = distribute(scenarios, max_runners) +def pairs_for_runner( + pairs: list[TestPair], runner_index: int, max_runners: int +) -> list[TestPair]: + """Return the subset of pairs assigned to *runner_index*.""" + groups = distribute(pairs, max_runners) if runner_index < 0 or runner_index >= len(groups): raise click.ClickException( _( @@ -84,19 +127,31 @@ def scenarios_for_runner( is_flag=True, help="List all discovered scenarios, one per line.", ) -def cli(runner_index: int | None, max_runners: int, list_all: bool) -> None: +@click.option( + "--list-platforms", + "list_platforms", + is_flag=True, + help="List all supported platforms, one per line.", +) +def cli(runner_index: int | None, max_runners: int, list_all: bool, list_platforms: bool) -> None: scenarios = discover_scenarios() if list_all: for s in scenarios: click.echo(s) return - if runner_index is None: - groups = distribute(scenarios, max_runners) - for i, group in enumerate(groups): - click.echo(f"Runner {i}: {' '.join(group) if group else '(none)'}") + if list_platforms: + for p in PLATFORMS: + click.echo(f"{p['name']}|{p['image']}|{p['command']}") return - assigned = scenarios_for_runner(scenarios, runner_index, max_runners) - click.echo(" ".join(assigned)) + pairs = build_pairs(scenarios) + if runner_index is None: + groups = distribute(pairs, max_runners) + for i, group in enumerate(groups): + labels = " ".join(p.encode() for p in group) if group else "(none)" + click.echo(f"Runner {i}: {labels}") + return + assigned = pairs_for_runner(pairs, runner_index, max_runners) + click.echo(" ".join(p.encode() for p in assigned)) if __name__ == "__main__": # pragma: no cover diff --git a/scripts/install_checkmake.py b/scripts/install_checkmake.py index b1d6080..d978931 100644 --- a/scripts/install_checkmake.py +++ b/scripts/install_checkmake.py @@ -10,10 +10,8 @@ from __future__ import annotations import platform import shutil import subprocess # nosec B404 -import sys -from pathlib import Path - import urllib.request +from pathlib import Path import click diff --git a/scripts/molecule_all.sh b/scripts/molecule_all.sh new file mode 100755 index 0000000..051a594 --- /dev/null +++ b/scripts/molecule_all.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Run all molecule scenarios on all supported OS platforms. +# Used by `make molecule-all`. Sequential — CI uses parallel matrix instead. +set -euo pipefail + +MOLECULE_BIN="$(realpath "${BIN:-.venv/bin}/molecule")" +ROLE_DIR="$(cd "$(dirname "$0")/.." && pwd)/ansible/roles/gitea-runner" + +for p in \ + ubuntu-2204:geerlingguy/docker-ubuntu2204-ansible:latest: \ + ubuntu-2404:geerlingguy/docker-ubuntu2404-ansible:latest: \ + debian-12:geerlingguy/docker-debian12-ansible:latest: \ + archlinux:marcstraube/archlinux-ansible:latest:/usr/lib/systemd/systemd +do + IFS=":" read -r name image command <<< "$p" + export MOLECULE_PLATFORM_NAME="$name" MOLECULE_PLATFORM_IMAGE="$image" + if [ -n "$command" ]; then + export MOLECULE_PLATFORM_COMMAND="$command" + else + unset MOLECULE_PLATFORM_COMMAND + fi + echo "=== Platform: $name ===" + for s in default multi-instance lifecycle template-content deregister update; do + echo "--- Scenario: $s on $name ---" + ( + cd "$ROLE_DIR" + if [ "$s" = "default" ]; then + ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 "$MOLECULE_BIN" test + else + ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 "$MOLECULE_BIN" test -s "$s" + fi + ) + done +done diff --git a/src/gitea_runner_manager/api_clients.py b/src/gitea_runner_manager/api_clients.py index 0cb692b..36e6fa7 100644 --- a/src/gitea_runner_manager/api_clients.py +++ b/src/gitea_runner_manager/api_clients.py @@ -116,6 +116,16 @@ class GiteaClient: payload = {"Do": "squash", "MergeTitleField": merge_title} self._request("POST", f"/pulls/{pr_number}/merge", json=payload) + def get_commit_status(self, sha: str) -> list[dict[str, Any]]: + """Fetch all status check contexts reported for a commit.""" + r = self._request("GET", f"/commits/{sha}/statuses") + return r.json() + + def get_pr(self, pr_number: str | int) -> dict[str, Any]: + """Fetch pull request details including mergeable state.""" + r = self._request("GET", f"/pulls/{pr_number}") + return r.json() + def create_release( self, tag: str, diff --git a/src/gitea_runner_manager/cli.py b/src/gitea_runner_manager/cli.py index 683b3d5..85a41a2 100644 --- a/src/gitea_runner_manager/cli.py +++ b/src/gitea_runner_manager/cli.py @@ -69,13 +69,6 @@ def cli() -> None: default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"), help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"), ) -@click.option( - "--mode", - "-m", - type=click.Choice(["docker", "binary"]), - default="docker", - help=_("Gitea Runner deployment mode (default: docker)"), -) @click.option( "--url", default=lambda: os.getenv("GITEA_URL", ""), @@ -111,7 +104,6 @@ def install( key: str | None, name: str | None, token: str | None, - mode: str, url: str, admin_token: str | None, integration_retries: int, @@ -127,7 +119,6 @@ def install( name=name, token=token, gitea_url=url, - mode=mode, admin_token=admin_token, integration_retries=integration_retries, labels=labels or None, @@ -147,13 +138,6 @@ def install( ) @click.option("--key", "-k", default=lambda: os.getenv("GITEA_RUNNER_KEY"), help=_("Path to SSH private key")) @click.option("--version", "-v", help=_("Specific Gitea Runner version")) -@click.option( - "--mode", - "-m", - type=click.Choice(["docker", "binary"]), - default="docker", - help=_("Gitea Runner deployment mode (default: docker)"), -) @click.option( "--ask-become-pass/--no-ask-become-pass", default=True, @@ -165,7 +149,6 @@ def update( user: str, key: str | None, version: str | None, - mode: str, ask_become_pass: bool, ) -> None: manager = RunnerManager() @@ -174,7 +157,6 @@ def update( user=user, key=key, version=version, - mode=mode, ask_become_pass=ask_become_pass, ) @@ -182,19 +164,12 @@ def update( @cli.command(help=_("Start a registered Gitea Runner.")) @click.argument("runner_name") @_runner_options -@click.option( - "--mode", - "-m", - type=click.Choice(["docker", "binary"]), - help=_("Override deployment mode from registry"), -) @_handle_errors("Start failed: {error}") def start( runner_name: str, host: str | None, user: str | None, key: str | None, - mode: str | None, ask_become_pass: bool, ) -> None: manager = RunnerManager() @@ -203,7 +178,6 @@ def start( host=host, user=user, key=key, - mode=mode, ask_become_pass=ask_become_pass, ) @@ -259,12 +233,6 @@ def enable( default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"), help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"), ) -@click.option( - "--mode", - "-m", - type=click.Choice(["docker", "binary"]), - help=_("Override deployment mode from registry"), -) @click.option( "--url", default=lambda: os.getenv("GITEA_URL", ""), @@ -277,7 +245,6 @@ def disable( user: str | None, key: str | None, token: str | None, - mode: str | None, url: str, ask_become_pass: bool, ) -> None: @@ -289,7 +256,6 @@ def disable( key=key, token=token, gitea_url=url, - mode=mode, ask_become_pass=ask_become_pass, ) @@ -297,19 +263,12 @@ def disable( @cli.command(help=_("Check the status of a registered Gitea Runner.")) @click.argument("runner_name") @_runner_options -@click.option( - "--mode", - "-m", - type=click.Choice(["docker", "binary"]), - help=_("Override deployment mode from registry"), -) @_handle_errors("Status check failed: {error}") def status( runner_name: str, host: str | None, user: str | None, key: str | None, - mode: str | None, ask_become_pass: bool, ) -> None: manager = RunnerManager() @@ -318,7 +277,6 @@ def status( host=host, user=user, key=key, - mode=mode, ask_become_pass=ask_become_pass, ) @@ -332,12 +290,6 @@ def status( default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"), help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"), ) -@click.option( - "--mode", - "-m", - type=click.Choice(["docker", "binary"]), - help=_("Override deployment mode from registry"), -) @click.option( "--url", default=lambda: os.getenv("GITEA_URL", ""), @@ -356,7 +308,6 @@ def remove( user: str | None, key: str | None, token: str | None, - mode: str | None, url: str, force: bool, ask_become_pass: bool, @@ -369,7 +320,6 @@ def remove( key=key, token=token, gitea_url=url, - mode=mode, ask_become_pass=ask_become_pass, force=force, ) @@ -385,7 +335,7 @@ def list_runners() -> None: click.echo(_("No runners registered. Use 'grm install' to add one.")) return - click.echo(f"{_('NAME'):<18} {_('HOST'):<16} {_('USER'):<10} {_('MODE'):<8} {_('LABELS'):<30} {_('STATUS')}") - click.echo("-" * 100) + click.echo(f"{_('NAME'):<18} {_('HOST'):<16} {_('USER'):<10} {_('LABELS'):<30} {_('STATUS')}") + click.echo("-" * 90) for r in runners: - click.echo(f"{r['name']:<18} {r['host']:<16} {r['user']:<10} {r['mode']:<8} {r['labels']:<30} {r['status']}") + click.echo(f"{r['name']:<18} {r['host']:<16} {r['user']:<10} {r['labels']:<30} {r['status']}") diff --git a/src/gitea_runner_manager/config.py b/src/gitea_runner_manager/config.py index a1fb2fb..dc8fb59 100644 --- a/src/gitea_runner_manager/config.py +++ b/src/gitea_runner_manager/config.py @@ -24,7 +24,7 @@ BRANCH_PROTECTION_CONFIG: dict[str, object] = { "branch_name": "master", "enable_push": False, "enable_status_check": True, - "status_check_contexts": ["lint", "unit-tests", "molecule-tests"], + "status_check_contexts": ["CI / quality", "CI / molecule-tests*"], "required_approvals": 1, "dismiss_stale_approvals": True, "block_on_outdated_branch": True, diff --git a/src/gitea_runner_manager/registry.py b/src/gitea_runner_manager/registry.py index bb82147..0937346 100644 --- a/src/gitea_runner_manager/registry.py +++ b/src/gitea_runner_manager/registry.py @@ -1,6 +1,6 @@ """Local JSON registry for installed Gitea Runners. -Stores connection metadata (host, user, key, mode) on the local machine +Stores connection metadata (host, user, key, labels) on the local machine so that subsequent lifecycle commands only need the runner name. """ @@ -44,7 +44,6 @@ class RunnerRegistry: host: str, user: str, key: str | None = None, - mode: str = "docker", gitea_url: str = "", labels: str | None = None, ) -> None: @@ -53,7 +52,6 @@ class RunnerRegistry: "host": host, "user": user, "key": key, - "mode": mode, "gitea_url": gitea_url, "labels": labels, "created_at": datetime.now(UTC).isoformat(), diff --git a/src/gitea_runner_manager/runner_manager.py b/src/gitea_runner_manager/runner_manager.py index bdbdf6c..37ed298 100644 --- a/src/gitea_runner_manager/runner_manager.py +++ b/src/gitea_runner_manager/runner_manager.py @@ -31,7 +31,6 @@ class RunnerManager: name: str | None = None, token: str | None = None, gitea_url: str = "", - mode: str = "docker", admin_token: str | None = None, integration_retries: int = 3, ask_become_pass: bool = False, @@ -47,7 +46,6 @@ class RunnerManager: extra_vars = ( f"registration_token={token} runner_name={name} gitea_url={gitea_url}" - f" runner_mode={mode}" f" gitea_runner_integration_retries={integration_retries}" ) if admin_token: @@ -67,7 +65,6 @@ class RunnerManager: host=host, user=user, key=key, - mode=mode, gitea_url=gitea_url, labels=labels, ) @@ -79,11 +76,10 @@ class RunnerManager: user: str, key: str | None = None, version: str | None = None, - mode: str = "docker", ask_become_pass: bool = False, ) -> None: """Update the gitea_runner binary on a remote host.""" - extra_vars = f"runner_mode={mode}" + extra_vars = "" if version: extra_vars += f" gitea_runner_version={version}" @@ -99,11 +95,10 @@ class RunnerManager: host: str | None = None, user: str | None = None, key: str | None = None, - mode: str | None = None, - ) -> tuple[str, str, str | None, str, str]: + ) -> tuple[str, str, str | None, str]: """Look up runner metadata from registry, applying CLI overrides. - Returns ``(host, user, key, mode, gitea_url)`` where *gitea_url* is + Returns ``(host, user, key, gitea_url)`` where *gitea_url* is taken from the registry when available, allowing ``disable`` and ``remove`` to reuse the value stored at install time. """ @@ -114,12 +109,10 @@ class RunnerManager: actual_host = host actual_user = user actual_key = key - actual_mode = mode or "docker" elif info: actual_host = host or info["host"] actual_user = user or info["user"] actual_key = key if key is not None else info.get("key") - actual_mode = mode or info.get("mode", "docker") else: raise AnsibleError( _( @@ -127,7 +120,7 @@ class RunnerManager: name=name, ) ) - return actual_host, actual_user, actual_key, actual_mode, actual_gitea_url + return actual_host, actual_user, actual_key, actual_gitea_url def start( self, @@ -135,16 +128,13 @@ class RunnerManager: host: str | None = None, user: str | None = None, key: str | None = None, - mode: str | None = None, ask_become_pass: bool = False, ) -> None: """Start a runner instance on a remote host.""" - actual_host, actual_user, actual_key, actual_mode, _gitea_url = self._resolve_runner( - name, host, user, key, mode - ) + actual_host, actual_user, actual_key, _gitea_url = self._resolve_runner(name, host, user, key) with track_steps() as tracker: tracker.begin(_("Starting Gitea Runner {name} on {host}", name=name, host=actual_host)) - extra_vars = f"runner_name={name} runner_mode={actual_mode}" + extra_vars = f"runner_name={name}" cmd = self._build_cmd("start-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass) self._executor.run( cmd, description=_("Starting Gitea Runner {name} on {host}", name=name, host=actual_host) @@ -160,7 +150,7 @@ class RunnerManager: ask_become_pass: bool = False, ) -> None: """Stop a runner instance on a remote host.""" - actual_host, actual_user, actual_key, _mode, _gitea_url = self._resolve_runner(name, host, user, key) + actual_host, actual_user, actual_key, _gitea_url = self._resolve_runner(name, host, user, key) with track_steps() as tracker: tracker.begin(_("Stopping Gitea Runner {name} on {host}", name=name, host=actual_host)) extra_vars = f"runner_name={name}" @@ -179,7 +169,7 @@ class RunnerManager: ask_become_pass: bool = False, ) -> None: """Enable a runner instance to start on boot.""" - actual_host, actual_user, actual_key, _mode, _gitea_url = self._resolve_runner(name, host, user, key) + actual_host, actual_user, actual_key, _gitea_url = self._resolve_runner(name, host, user, key) with track_steps() as tracker: tracker.begin(_("Enabling Gitea Runner {name} on {host}", name=name, host=actual_host)) extra_vars = f"runner_name={name}" @@ -199,13 +189,10 @@ class RunnerManager: key: str | None = None, token: str | None = None, gitea_url: str = "", - mode: str | None = None, ask_become_pass: bool = False, ) -> None: """Disable and deregister a runner instance.""" - actual_host, actual_user, actual_key, actual_mode, registry_gitea_url = self._resolve_runner( - name, host, user, key, mode - ) + actual_host, actual_user, actual_key, registry_gitea_url = self._resolve_runner(name, host, user, key) resolved_gitea_url = gitea_url or registry_gitea_url if not resolved_gitea_url: raise AnsibleError(_("GITEA_URL must be set (or pass --url)")) @@ -213,10 +200,7 @@ class RunnerManager: raise AnsibleError(_("GITEA_REGISTRATION_TOKEN must be set (or pass --token)")) with track_steps() as tracker: tracker.begin(_("Disabling Gitea Runner {name} on {host}", name=name, host=actual_host)) - extra_vars = ( - f"runner_name={name} registration_token={token}" - f" gitea_url={resolved_gitea_url} runner_mode={actual_mode}" - ) + extra_vars = f"runner_name={name} registration_token={token} gitea_url={resolved_gitea_url}" cmd = self._build_cmd( "disable-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass ) @@ -231,16 +215,13 @@ class RunnerManager: host: str | None = None, user: str | None = None, key: str | None = None, - mode: str | None = None, ask_become_pass: bool = False, ) -> None: """Check the status of a runner instance.""" - actual_host, actual_user, actual_key, actual_mode, _gitea_url = self._resolve_runner( - name, host, user, key, mode - ) + actual_host, actual_user, actual_key, _gitea_url = self._resolve_runner(name, host, user, key) with track_steps() as tracker: tracker.begin(_("Checking status of Gitea Runner {name} on {host}", name=name, host=actual_host)) - extra_vars = f"runner_name={name} runner_mode={actual_mode}" + extra_vars = f"runner_name={name}" cmd = self._build_cmd( "status-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass ) @@ -257,7 +238,6 @@ class RunnerManager: key: str | None = None, token: str | None = None, gitea_url: str = "", - mode: str | None = None, ask_become_pass: bool = False, force: bool = False, ) -> None: @@ -267,9 +247,7 @@ class RunnerManager: only remove the local registry entry. Use this when the remote host is already gone or unreachable. """ - actual_host, actual_user, actual_key, actual_mode, registry_gitea_url = self._resolve_runner( - name, host, user, key, mode - ) + actual_host, actual_user, actual_key, registry_gitea_url = self._resolve_runner(name, host, user, key) resolved_gitea_url = gitea_url or registry_gitea_url resolved_token = token if not force: @@ -280,10 +258,7 @@ class RunnerManager: with track_steps() as tracker: tracker.begin(_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host)) if not force: - extra_vars = ( - f"runner_name={name} registration_token={resolved_token}" - f" gitea_url={resolved_gitea_url} runner_mode={actual_mode}" - ) + extra_vars = f"runner_name={name} registration_token={resolved_token} gitea_url={resolved_gitea_url}" cmd = self._build_cmd( "remove-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass ) @@ -304,7 +279,6 @@ class RunnerManager: host = info["host"] user = info["user"] key = info.get("key") - mode = info.get("mode", "docker") say( _( "Checking status of Gitea Runner {name} on {host} as {user} (sudo required)", @@ -315,64 +289,20 @@ class RunnerManager: ) service_status = "unknown" - if mode == "docker": - # Try expected container name first, then host-based fallback for legacy installs. - # Use python3 JSON parsing to avoid Jinja2 brace conflicts in Ansible shell args. - for container_name in (f"gitea-runner-{name}", f"gitea-runner-{host}"): - try: - stdout = self._executor.run_ad_hoc( - host, - user, - key, - "shell", - ( - f"docker inspect {container_name} 2>/dev/null | " - f"python3 -c \"import sys,json; print(json.load(sys.stdin)[0]['State']['Status'])\" " - f"2>/dev/null || echo unknown" - ), - become=True, - ask_become_pass=True, - check=False, - ) - except AnsibleError: - continue - status = self._parse_status(stdout) - if status != "unknown": - docker_map = {"running": "active", "exited": "inactive", "dead": "failed"} - service_status = docker_map.get(status, "unknown") - break - else: - # Final fallback: check systemd service (for installs using the fixed template) - try: - stdout = self._executor.run_ad_hoc( - host, - user, - key, - "shell", - f"systemctl is-active gitea-runner@{name} 2>/dev/null", - become=True, - ask_become_pass=True, - check=False, - ) - service_status = self._parse_status(stdout) - except AnsibleError: - service_status = "unknown" - else: - try: - stdout = self._executor.run_ad_hoc( - host, - user, - key, - "shell", - f"systemctl is-active gitea-runner@{name} 2>/dev/null", - become=True, - ask_become_pass=True, - check=False, - ) - service_status = self._parse_status(stdout) - except AnsibleError: - service_status = "unknown" - # Translate known status values + try: + stdout = self._executor.run_ad_hoc( + host, + user, + key, + "shell", + f"sudo -u grm-{name} systemctl --user is-active gitea-runner 2>/dev/null", + become=True, + ask_become_pass=True, + check=False, + ) + service_status = self._parse_status(stdout) + except AnsibleError: + service_status = "unknown" translated_status = _( service_status if service_status in {"active", "inactive", "failed", "unknown"} else "unknown" ) @@ -381,7 +311,6 @@ class RunnerManager: "name": name, "host": host, "user": user, - "mode": mode, "labels": info.get("labels", ""), "status": translated_status, } diff --git a/tests/integration/test_lifecycle.py b/tests/integration/test_lifecycle.py index 4b54de6..f6d791f 100644 --- a/tests/integration/test_lifecycle.py +++ b/tests/integration/test_lifecycle.py @@ -27,46 +27,55 @@ class TestLifecycleCLI: mock_manager.install.assert_called_once() # Start - result = runner.invoke(cli, ["start", "host1", "--user", "ubuntu", "--name", "r1"]) + result = runner.invoke(cli, ["start", "r1", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"]) assert result.exit_code == 0 mock_manager.start.assert_called_once_with( - host="host1", user="ubuntu", name="r1", mode="docker", ask_become_pass=False + host="host1", user="ubuntu", name="r1", key=None, ask_become_pass=False ) # Status - result = runner.invoke(cli, ["status", "host1", "--user", "ubuntu", "--name", "r1"]) + result = runner.invoke(cli, ["status", "r1", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"]) assert result.exit_code == 0 mock_manager.status.assert_called_once_with( - host="host1", user="ubuntu", name="r1", mode="docker", ask_become_pass=False + host="host1", user="ubuntu", name="r1", key=None, ask_become_pass=False ) # Stop - result = runner.invoke(cli, ["stop", "host1", "--user", "ubuntu", "--name", "r1"]) + result = runner.invoke(cli, ["stop", "r1", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"]) assert result.exit_code == 0 - mock_manager.stop.assert_called_once_with(host="host1", user="ubuntu", name="r1", ask_become_pass=False) + mock_manager.stop.assert_called_once_with( + host="host1", user="ubuntu", name="r1", key=None, ask_become_pass=False + ) # Disable - result = runner.invoke(cli, ["disable", "host1", "--user", "ubuntu", "--name", "r1", "--token", "tok"]) + result = runner.invoke( + cli, + ["disable", "r1", "--host", "host1", "--user", "ubuntu", "--token", "tok", "--no-ask-become-pass"], + ) assert result.exit_code == 0 mock_manager.disable.assert_called_once_with( host="host1", user="ubuntu", name="r1", + key=None, token="tok", gitea_url="https://git.example.com", - mode="docker", ask_become_pass=False, ) # Remove - result = runner.invoke(cli, ["remove", "host1", "--user", "ubuntu", "--name", "r1", "--token", "tok"]) + result = runner.invoke( + cli, + ["remove", "r1", "--host", "host1", "--user", "ubuntu", "--token", "tok", "--no-ask-become-pass"], + ) assert result.exit_code == 0 mock_manager.remove.assert_called_once_with( host="host1", user="ubuntu", name="r1", + key=None, token="tok", gitea_url="https://git.example.com", - mode="docker", ask_become_pass=False, + force=False, ) diff --git a/tests/integration/test_multi_instance.py b/tests/integration/test_multi_instance.py index f445535..b3d84b4 100644 --- a/tests/integration/test_multi_instance.py +++ b/tests/integration/test_multi_instance.py @@ -26,15 +26,13 @@ class TestMultiInstanceCLI: assert result.exit_code == 0 # Install instance B - result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--name", "runner-b", "--mode", "binary"]) + result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--name", "runner-b"]) assert result.exit_code == 0 assert mock_manager.install.call_count == 2 calls = mock_manager.install.call_args_list assert calls[0].kwargs["name"] == "runner-a" - assert calls[0].kwargs["mode"] == "docker" assert calls[1].kwargs["name"] == "runner-b" - assert calls[1].kwargs["mode"] == "binary" @patch("gitea_runner_manager.cli.RunnerManager") def test_start_stop_one_instance(self, mock_manager_class: MagicMock) -> None: @@ -44,12 +42,16 @@ class TestMultiInstanceCLI: runner = CliRunner() - result = runner.invoke(cli, ["start", "host1", "--user", "ubuntu", "--name", "runner-a"]) + result = runner.invoke( + cli, ["start", "runner-a", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"] + ) assert result.exit_code == 0 mock_manager.start.assert_called_once_with( - host="host1", user="ubuntu", name="runner-a", mode="docker", ask_become_pass=False + host="host1", user="ubuntu", name="runner-a", key=None, ask_become_pass=False ) - result = runner.invoke(cli, ["stop", "host1", "--user", "ubuntu", "--name", "runner-b"]) + result = runner.invoke(cli, ["stop", "runner-b", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"]) assert result.exit_code == 0 - mock_manager.stop.assert_called_once_with(host="host1", user="ubuntu", name="runner-b", ask_become_pass=False) + mock_manager.stop.assert_called_once_with( + host="host1", user="ubuntu", name="runner-b", key=None, ask_become_pass=False + ) diff --git a/tests/unit/test_api_clients.py b/tests/unit/test_api_clients.py index e701fe1..44d2482 100644 --- a/tests/unit/test_api_clients.py +++ b/tests/unit/test_api_clients.py @@ -201,6 +201,33 @@ class TestGiteaClient: timeout=DEFAULT_TIMEOUT, ) + def test_get_commit_status(self) -> None: + client = GiteaClient("https://git.example.com", "tok", "owner", "repo") + client._session.request = MagicMock( + return_value=_mock_response([{"context": "CI / quality", "status": "success"}]) + ) + + result = client.get_commit_status("abc123") + assert result == [{"context": "CI / quality", "status": "success"}] + client._session.request.assert_called_once_with( + "GET", + "https://git.example.com/repos/owner/repo/commits/abc123/statuses", + timeout=DEFAULT_TIMEOUT, + ) + + def test_get_pr(self) -> None: + client = GiteaClient("https://git.example.com", "tok", "owner", "repo") + client._session.request = MagicMock(return_value=_mock_response({"number": 7, "head": {"sha": "abc123"}})) + + result = client.get_pr(7) + assert result["number"] == 7 + assert result["head"]["sha"] == "abc123" + client._session.request.assert_called_once_with( + "GET", + "https://git.example.com/repos/owner/repo/pulls/7", + timeout=DEFAULT_TIMEOUT, + ) + def test_update_repo_settings(self) -> None: client = GiteaClient("https://git.example.com", "tok", "owner", "repo") client._session.request = MagicMock(return_value=_mock_response({"default_delete_branch_after_merge": True})) diff --git a/tests/unit/test_auto_merge.py b/tests/unit/test_auto_merge.py index 8bdbf0a..ac1719f 100644 --- a/tests/unit/test_auto_merge.py +++ b/tests/unit/test_auto_merge.py @@ -9,7 +9,21 @@ from click.testing import CliRunner from gitea_runner_manager.config import CONVENTIONAL_RE, TASK_ID_RE from gitea_runner_manager.exceptions import APIError -from scripts.auto_merge import extract_task_id, has_ready_to_merge_label, main, validate_pr_title +from scripts.auto_merge import ( + extract_task_id, + has_ready_to_merge_label, + main, + validate_pr_title, + wait_for_ci, +) + +CI_SUCCESS = "success" +CI_PENDING = "pending" +CI_FAILURE = "failure" + + +def _status(context: str, status: str, updated_at: str = "2026-01-01T00:00:00Z") -> dict[str, str]: + return {"context": context, "status": status, "updated_at": updated_at} class TestRegexes: @@ -70,12 +84,91 @@ class TestHasReadyToMergeLabel: assert has_ready_to_merge_label(client, "5") is False +class TestWaitForCi: + def test_all_pass_immediately(self) -> None: + client = MagicMock() + client.get_commit_status.return_value = [ + _status("CI / quality (pull_request)", CI_SUCCESS), + _status("CI / molecule-tests (0) (pull_request)", CI_SUCCESS), + ] + assert wait_for_ci(client, "abc123", max_wait=10) is True + + def test_waits_then_passes(self) -> None: + client = MagicMock() + client.get_commit_status.side_effect = [ + [_status("CI / quality (pull_request)", CI_PENDING)], + [_status("CI / quality (pull_request)", CI_SUCCESS)], + ] + with patch("scripts.auto_merge.time.sleep"): + assert wait_for_ci(client, "abc123", max_wait=10, poll_interval=5) is True + + def test_fails_on_failed_check(self) -> None: + client = MagicMock() + client.get_commit_status.return_value = [ + _status("CI / quality (pull_request)", CI_SUCCESS), + _status("CI / molecule-tests (0) (pull_request)", CI_FAILURE), + ] + assert wait_for_ci(client, "abc123", max_wait=10) is False + + def test_times_out(self) -> None: + client = MagicMock() + client.get_commit_status.return_value = [ + _status("CI / quality (pull_request)", CI_PENDING), + ] + with patch("scripts.auto_merge.time.sleep"): + assert wait_for_ci(client, "abc123", max_wait=5) is False + + def test_no_statuses_waits(self) -> None: + client = MagicMock() + client.get_commit_status.side_effect = [ + [], + [_status("CI / quality (pull_request)", CI_SUCCESS)], + ] + with patch("scripts.auto_merge.time.sleep"): + assert wait_for_ci(client, "abc123", max_wait=10, poll_interval=5) is True + + def test_ignores_non_ci_contexts(self) -> None: + client = MagicMock() + client.get_commit_status.return_value = [ + _status("Auto-merge / merge (pull_request)", CI_PENDING), + _status("CI / quality (pull_request)", CI_SUCCESS), + ] + assert wait_for_ci(client, "abc123", max_wait=10) is True + + def test_deduplicates_by_latest(self) -> None: + client = MagicMock() + client.get_commit_status.return_value = [ + _status("CI / quality (pull_request)", CI_PENDING, "2026-01-01T00:00:00Z"), + _status("CI / quality (pull_request)", CI_SUCCESS, "2026-01-01T00:01:00Z"), + ] + assert wait_for_ci(client, "abc123", max_wait=10) is True + + def test_only_non_ci_contexts_waits_then_ci_appears(self) -> None: + client = MagicMock() + client.get_commit_status.side_effect = [ + [_status("Auto-merge / merge (pull_request)", CI_PENDING)], + [_status("CI / quality (pull_request)", CI_SUCCESS)], + ] + with patch("scripts.auto_merge.time.sleep"): + assert wait_for_ci(client, "abc123", max_wait=10, poll_interval=5) is True + + +def _mock_pr(sha: str = "abc123def456") -> dict[str, object]: + return {"head": {"sha": sha}} + + +def _mock_ci_passing() -> list[dict[str, str]]: + return [_status("CI / quality (pull_request)", CI_SUCCESS)] + + class TestMain: @patch.dict("os.environ", {"REPO_TOKEN": "tok"}) @patch("scripts.auto_merge.GiteaClient") def test_successful_flow_with_label_arg(self, mock_client_cls: MagicMock) -> None: mock_client = MagicMock() mock_client_cls.return_value = mock_client + mock_client.get_pr.return_value = _mock_pr() + mock_client.get_commit_status.return_value = _mock_ci_passing() runner = CliRunner() result = runner.invoke( main, @@ -91,6 +184,8 @@ class TestMain: """Label not passed via arg, but PR has ready-to-merge via API.""" mock_client = MagicMock() mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}] + mock_client.get_pr.return_value = _mock_pr() + mock_client.get_commit_status.return_value = _mock_ci_passing() mock_client_cls.return_value = mock_client runner = CliRunner() result = runner.invoke( @@ -123,6 +218,8 @@ class TestMain: """Gitea Actions doesn't populate label name, but API shows ready-to-merge.""" mock_client = MagicMock() mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}] + mock_client.get_pr.return_value = _mock_pr() + mock_client.get_commit_status.return_value = _mock_ci_passing() mock_client_cls.return_value = mock_client runner = CliRunner() result = runner.invoke( @@ -166,6 +263,8 @@ class TestMain: def test_merge_pr_failure_raises_click(self, mock_client_cls: MagicMock) -> None: mock_client = MagicMock() mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}] + mock_client.get_pr.return_value = _mock_pr() + mock_client.get_commit_status.return_value = _mock_ci_passing() mock_client.merge_pr.side_effect = APIError(http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error") mock_client_cls.return_value = mock_client runner = CliRunner() @@ -178,9 +277,41 @@ class TestMain: def test_merge_pr_json_parse_failure(self, mock_client_cls: MagicMock) -> None: mock_client = MagicMock() mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}] + mock_client.get_pr.return_value = _mock_pr() + mock_client.get_commit_status.return_value = _mock_ci_passing() mock_client.merge_pr.side_effect = APIError(http.HTTPStatus.BAD_GATEWAY, "bad gateway") mock_client_cls.return_value = mock_client runner = CliRunner() result = runner.invoke(main, ["GRM-19-fix", "fix: bug", "owner/repo", "1"]) assert result.exit_code == 1 assert str(http.HTTPStatus.BAD_GATEWAY) in result.output + + @patch.dict("os.environ", {"REPO_TOKEN": "tok"}) + @patch("scripts.auto_merge.GiteaClient") + def test_ci_failure_blocks_merge(self, mock_client_cls: MagicMock) -> None: + """CI checks fail — merge should not be attempted.""" + mock_client = MagicMock() + mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}] + mock_client.get_pr.return_value = _mock_pr() + mock_client.get_commit_status.return_value = [ + _status("CI / quality (pull_request)", CI_FAILURE), + ] + mock_client_cls.return_value = mock_client + runner = CliRunner() + result = runner.invoke(main, ["GRM-19-fix", "fix: bug", "owner/repo", "1"]) + assert result.exit_code == 1 + assert "CI checks did not pass" in result.output + mock_client.merge_pr.assert_not_called() + + @patch.dict("os.environ", {"REPO_TOKEN": "tok"}) + @patch("scripts.auto_merge.GiteaClient") + def test_no_sha_proceeds_without_wait(self, mock_client_cls: MagicMock) -> None: + """PR head SHA missing — should proceed without waiting.""" + mock_client = MagicMock() + mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}] + mock_client.get_pr.return_value = {"head": {}} + mock_client_cls.return_value = mock_client + runner = CliRunner() + result = runner.invoke(main, ["GRM-19-fix", "fix: bug", "owner/repo", "1"]) + assert result.exit_code == 0 + assert "squash-merged" in result.output diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index cde06d8..fc45759 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -29,7 +29,6 @@ class TestCLI: name=None, token="tok", gitea_url="https://git.example.com", - mode="docker", admin_token="", integration_retries=3, labels=None, @@ -51,7 +50,6 @@ class TestCLI: name=None, token="tok", gitea_url="https://git.example.com", - mode="docker", admin_token="", integration_retries=3, labels=None, @@ -91,7 +89,6 @@ class TestCLI: name=None, token="tok", gitea_url="https://git.example.com", - mode="docker", admin_token=None, integration_retries=3, labels=None, @@ -141,7 +138,6 @@ class TestCLI: name="r1", token="tok", gitea_url="https://git.example.com", - mode="docker", admin_token="", integration_retries=3, labels=None, @@ -163,29 +159,6 @@ class TestCLI: name=None, token="tok", gitea_url="https://git.example.com", - mode="docker", - admin_token="", - integration_retries=3, - labels=None, - ask_become_pass=True, - ) - - @patch("gitea_runner_manager.cli.RunnerManager") - def test_install_binary_mode(self, mock_manager_class: MagicMock) -> None: - mock_manager = MagicMock() - mock_manager_class.return_value = mock_manager - - runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": "", "GITEA_RUNNER_LABELS": ""}) - result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok", "--mode", "binary"]) - assert result.exit_code == 0 - mock_manager.install.assert_called_once_with( - host="host1", - user="ubuntu", - key=None, - name=None, - token="tok", - gitea_url="https://git.example.com", - mode="binary", admin_token="", integration_retries=3, labels=None, @@ -230,7 +203,6 @@ class TestCLI: user="ubuntu", key="/key", version="v0.2.0", - mode="docker", ask_become_pass=True, ) @@ -247,24 +219,6 @@ class TestCLI: user="ubuntu", key=None, version=None, - mode="docker", - ask_become_pass=True, - ) - - @patch("gitea_runner_manager.cli.RunnerManager") - def test_update_binary_mode(self, mock_manager_class: MagicMock) -> None: - mock_manager = MagicMock() - mock_manager_class.return_value = mock_manager - - runner = CliRunner() - result = runner.invoke(cli, ["update", "host1", "--user", "ubuntu", "--mode", "binary"]) - assert result.exit_code == 0 - mock_manager.update.assert_called_once_with( - host="host1", - user="ubuntu", - key=None, - version=None, - mode="binary", ask_become_pass=True, ) @@ -294,7 +248,6 @@ class TestCLI: host=None, user=None, key=None, - mode=None, ask_become_pass=True, ) @@ -304,14 +257,13 @@ class TestCLI: mock_manager_class.return_value = mock_manager runner = CliRunner() - result = runner.invoke(cli, ["start", "r1", "--host", "newhost", "--user", "newuser", "--mode", "binary"]) + result = runner.invoke(cli, ["start", "r1", "--host", "newhost", "--user", "newuser"]) assert result.exit_code == 0 mock_manager.start.assert_called_once_with( name="r1", host="newhost", user="newuser", key=None, - mode="binary", ask_become_pass=True, ) @@ -362,7 +314,6 @@ class TestCLI: key=None, token="tok", gitea_url="https://git.example.com", - mode=None, ask_become_pass=True, ) @@ -396,7 +347,6 @@ class TestCLI: key=None, token="tok", gitea_url="https://git.example.com", - mode=None, ask_become_pass=True, ) @@ -426,7 +376,6 @@ class TestCLI: host=None, user=None, key=None, - mode=None, ask_become_pass=True, ) @@ -445,7 +394,6 @@ class TestCLI: key=None, token="tok", gitea_url="https://git.example.com", - mode=None, force=False, ask_become_pass=True, ) @@ -465,7 +413,6 @@ class TestCLI: key=None, token="tok", gitea_url="https://git.example.com", - mode=None, force=True, ask_become_pass=True, ) @@ -552,7 +499,6 @@ class TestCLI: key=None, token="tok", gitea_url="https://git.example.com", - mode=None, force=False, ask_become_pass=True, ) @@ -578,7 +524,6 @@ class TestCLI: "name": "r1", "host": "10.0.0.1", "user": "ubuntu", - "mode": "docker", "labels": "docker:docker://alpine:latest", "status": "active", }, diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 20876f9..60d463c 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -47,7 +47,7 @@ class TestConfigConstants: assert BRANCH_PROTECTION_CONFIG["branch_name"] == "master" assert BRANCH_PROTECTION_CONFIG["enable_push"] is False assert BRANCH_PROTECTION_CONFIG["required_approvals"] == 1 - assert BRANCH_PROTECTION_CONFIG["status_check_contexts"] == ["lint", "unit-tests", "molecule-tests"] + assert BRANCH_PROTECTION_CONFIG["status_check_contexts"] == ["CI / quality", "CI / molecule-tests*"] def test_label_config(self) -> None: assert LABEL_CONFIG["name"] == "ready-to-merge" diff --git a/tests/unit/test_distribute_molecule.py b/tests/unit/test_distribute_molecule.py index 861e9ff..47b86ec 100644 --- a/tests/unit/test_distribute_molecule.py +++ b/tests/unit/test_distribute_molecule.py @@ -8,9 +8,12 @@ import pytest from scripts.distribute_molecule import ( MOLECULE_ROOT, + PLATFORMS, + TestPair, + build_pairs, discover_scenarios, distribute, - scenarios_for_runner, + pairs_for_runner, ) @@ -33,47 +36,97 @@ class TestDiscoverScenarios: assert Path("ansible/roles/gitea-runner/molecule") == MOLECULE_ROOT +class TestPairEncoding: + def test_encode_roundtrip(self) -> None: + pair = TestPair("default", {"name": "ubuntu-2204", "image": "ubuntu:22.04", "command": ""}) + encoded = pair.encode() + assert encoded == "default|ubuntu-2204|ubuntu:22.04|" + decoded = TestPair.decode(encoded) + assert decoded.scenario == "default" + assert decoded.platform["name"] == "ubuntu-2204" + assert decoded.platform["image"] == "ubuntu:22.04" + assert decoded.platform["command"] == "" + + def test_encode_with_command(self) -> None: + pair = TestPair( + "default", + {"name": "archlinux", "image": "archlinux:latest", "command": "/usr/lib/systemd/systemd"}, + ) + encoded = pair.encode() + assert encoded == "default|archlinux|archlinux:latest|/usr/lib/systemd/systemd" + decoded = TestPair.decode(encoded) + assert decoded.platform["command"] == "/usr/lib/systemd/systemd" + + +class TestBuildPairs: + def test_cross_product(self) -> None: + scenarios = ["a", "b"] + platforms = [ + {"name": "p1", "image": "img1", "command": ""}, + {"name": "p2", "image": "img2", "command": ""}, + ] + pairs = build_pairs(scenarios, platforms) + assert len(pairs) == 4 + assert pairs[0].scenario == "a" + assert pairs[0].platform["name"] == "p1" + assert pairs[1].scenario == "a" + assert pairs[1].platform["name"] == "p2" + assert pairs[2].scenario == "b" + assert pairs[2].platform["name"] == "p1" + assert pairs[3].scenario == "b" + assert pairs[3].platform["name"] == "p2" + + def test_default_platforms(self) -> None: + pairs = build_pairs(["default"]) + assert len(pairs) == len(PLATFORMS) + assert all(p.scenario == "default" for p in pairs) + + class TestDistribute: def test_even_split(self) -> None: - scenarios = ["a", "b", "c", "d", "e", "f"] - groups = distribute(scenarios, 3) - assert groups == [["a", "d"], ["b", "e"], ["c", "f"]] + pairs = [TestPair(f"s{i}", {"name": "p", "image": "i", "command": ""}) for i in range(6)] + groups = distribute(pairs, 3) + assert len(groups) == 3 + assert len(groups[0]) == 2 + assert len(groups[1]) == 2 + assert len(groups[2]) == 2 def test_uneven_split(self) -> None: - scenarios = ["a", "b", "c", "d", "e"] - groups = distribute(scenarios, 3) - assert groups == [["a", "d"], ["b", "e"], ["c"]] + pairs = [TestPair(f"s{i}", {"name": "p", "image": "i", "command": ""}) for i in range(5)] + groups = distribute(pairs, 3) + assert len(groups[0]) == 2 + assert len(groups[1]) == 2 + assert len(groups[2]) == 1 - def test_more_runners_than_scenarios(self) -> None: - scenarios = ["a", "b"] - groups = distribute(scenarios, 5) - assert groups == [["a"], ["b"], [], [], []] + def test_more_runners_than_pairs(self) -> None: + pairs = [TestPair("a", {"name": "p", "image": "i", "command": ""})] + groups = distribute(pairs, 5) + assert len(groups) == 5 + assert len(groups[0]) == 1 + assert all(len(g) == 0 for g in groups[1:]) - def test_single_runner(self) -> None: - scenarios = ["a", "b", "c"] - groups = distribute(scenarios, 1) - assert groups == [["a", "b", "c"]] - - def test_empty_scenarios(self) -> None: + def test_empty_pairs(self) -> None: groups = distribute([], 3) assert groups == [[], [], []] -class TestScenariosForRunner: +class TestPairsForRunner: def test_returns_correct_subset(self) -> None: - scenarios = ["a", "b", "c", "d", "e", "f"] - assert scenarios_for_runner(scenarios, 0, 3) == ["a", "d"] - assert scenarios_for_runner(scenarios, 1, 3) == ["b", "e"] - assert scenarios_for_runner(scenarios, 2, 3) == ["c", "f"] + pairs = [TestPair(f"s{i}", {"name": "p", "image": "i", "command": ""}) for i in range(6)] + assert len(pairs_for_runner(pairs, 0, 3)) == 2 + assert len(pairs_for_runner(pairs, 1, 3)) == 2 + assert len(pairs_for_runner(pairs, 2, 3)) == 2 def test_out_of_range_raises(self) -> None: + pairs = [TestPair("a", {"name": "p", "image": "i", "command": ""})] with pytest.raises(click.ClickException) as exc: - scenarios_for_runner(["a"], 5, 3) + pairs_for_runner(pairs, 5, 3) assert "out of range" in str(exc.value) def test_negative_index_raises(self) -> None: + pairs = [TestPair("a", {"name": "p", "image": "i", "command": ""})] with pytest.raises(click.ClickException) as exc: - scenarios_for_runner(["a"], -1, 3) + pairs_for_runner(pairs, -1, 3) assert "out of range" in str(exc.value) @@ -93,6 +146,19 @@ class TestCli: assert "alpha" in result.output assert "beta" in result.output + def test_list_platforms_flag(self) -> None: + from click.testing import CliRunner + + from scripts.distribute_molecule import cli + + runner = CliRunner() + result = runner.invoke(cli, ["--list-platforms"]) + assert result.exit_code == 0 + assert "ubuntu-2204" in result.output + assert "ubuntu-2404" in result.output + assert "debian-12" in result.output + assert "archlinux" in result.output + def test_no_runner_index_prints_all_groups(self, tmp_path: Path) -> None: from click.testing import CliRunner @@ -115,13 +181,14 @@ class TestCli: from scripts.distribute_molecule import cli root = tmp_path / "molecule" - for s in ["a", "b", "c"]: - (root / s).mkdir(parents=True) + (root / "alpha").mkdir(parents=True) with patch("scripts.distribute_molecule.MOLECULE_ROOT", root): runner = CliRunner() - result = runner.invoke(cli, ["--runner-index", "1", "--max-runners", "3"]) + result = runner.invoke(cli, ["--runner-index", "0", "--max-runners", "3"]) assert result.exit_code == 0 - assert result.output.strip() == "b" + # Output should contain encoded pairs with platform info + assert "alpha|" in result.output + assert "ubuntu-2204" in result.output def test_main_module_block() -> None: diff --git a/tests/unit/test_registry.py b/tests/unit/test_registry.py index 3a65ab9..1be215f 100644 --- a/tests/unit/test_registry.py +++ b/tests/unit/test_registry.py @@ -22,7 +22,6 @@ class TestRunnerRegistry: host="10.0.0.1", user="ubuntu", key="/key", - mode="docker", gitea_url="https://git.example.com", labels="docker:docker://alpine:latest", ) @@ -31,7 +30,6 @@ class TestRunnerRegistry: assert info["host"] == "10.0.0.1" assert info["user"] == "ubuntu" assert info["key"] == "/key" - assert info["mode"] == "docker" assert info["gitea_url"] == "https://git.example.com" assert info["labels"] == "docker:docker://alpine:latest" assert "created_at" in info diff --git a/tests/unit/test_runner_manager.py b/tests/unit/test_runner_manager.py index de36066..ab548f6 100644 --- a/tests/unit/test_runner_manager.py +++ b/tests/unit/test_runner_manager.py @@ -33,14 +33,12 @@ class TestRunnerManager: assert "registration_token=tok" in cmd_str assert "runner_name=192.168.1.10" in cmd_str assert "gitea_url=https://git.example.com" in cmd_str - assert "runner_mode=docker" in cmd_str assert "Installing Gitea Runner on 192.168.1.10" in mock_executor.run.call_args.kwargs["description"] mock_registry.add.assert_called_once_with( name="192.168.1.10", host="192.168.1.10", user="ubuntu", key=None, - mode="docker", gitea_url="https://git.example.com", labels=None, ) @@ -60,14 +58,12 @@ class TestRunnerManager: assert "/key" in cmd_str assert "registration_token=preset" in cmd_str assert "runner_name=my-runner" in cmd_str - assert "runner_mode=docker" in cmd_str assert "--ask-become-pass" not in cmd_str mock_registry.add.assert_called_once_with( name="my-runner", host="host1", user="root", key="/key", - mode="docker", gitea_url="https://git.example.com", labels=None, ) @@ -82,27 +78,6 @@ class TestRunnerManager: cmd = mock_executor.run.call_args.args[0] cmd_str = " ".join(cmd) assert "--ask-become-pass" in cmd_str - assert "runner_mode=docker" in cmd_str - - def test_install_binary_mode(self) -> None: - mock_registry = MagicMock() - manager = RunnerManager(registry=mock_registry) - mock_executor = MagicMock() - manager._executor = mock_executor - - manager.install("host1", "root", token="tok", gitea_url="https://git.example.com", mode="binary") - cmd = mock_executor.run.call_args.args[0] - cmd_str = " ".join(cmd) - assert "runner_mode=binary" in cmd_str - mock_registry.add.assert_called_once_with( - name="host1", - host="host1", - user="root", - key=None, - mode="binary", - gitea_url="https://git.example.com", - labels=None, - ) def test_install_missing_gitea_url(self) -> None: manager = RunnerManager() @@ -181,7 +156,6 @@ class TestRunnerManager: assert "--private-key" in cmd_str assert "/key" in cmd_str assert "gitea_runner_version=v0.2.0" in cmd_str - assert "runner_mode=docker" in cmd_str assert "--ask-become-pass" not in cmd_str assert "Updating Gitea Runner on host" in mock_executor.run.call_args.kwargs["description"] @@ -194,17 +168,6 @@ class TestRunnerManager: cmd = mock_executor.run.call_args.args[0] cmd_str = " ".join(cmd) assert "--ask-become-pass" in cmd_str - assert "runner_mode=docker" in cmd_str - - def test_update_binary_mode(self) -> None: - manager = RunnerManager() - mock_executor = MagicMock() - manager._executor = mock_executor - - manager.update("host", "user", mode="binary") - cmd = mock_executor.run.call_args.args[0] - cmd_str = " ".join(cmd) - assert "runner_mode=binary" in cmd_str def test_update_playbook_not_found(self) -> None: manager = RunnerManager() @@ -226,15 +189,13 @@ class TestRunnerManager: "host": "10.0.0.1", "user": "ubuntu", "key": "/key", - "mode": "docker", "gitea_url": "https://git.example.com", } manager = RunnerManager(registry=mock_registry) - host, user, key, mode, gitea_url = manager._resolve_runner("r1") + host, user, key, gitea_url = manager._resolve_runner("r1") assert host == "10.0.0.1" assert user == "ubuntu" assert key == "/key" - assert mode == "docker" assert gitea_url == "https://git.example.com" mock_registry.get.assert_called_once_with("r1") @@ -242,11 +203,10 @@ class TestRunnerManager: mock_registry = MagicMock() mock_registry.get.return_value = None manager = RunnerManager(registry=mock_registry) - host, user, key, mode, gitea_url = manager._resolve_runner("r1", host="10.0.0.2", user="root") + host, user, key, gitea_url = manager._resolve_runner("r1", host="10.0.0.2", user="root") assert host == "10.0.0.2" assert user == "root" assert key is None - assert mode == "docker" assert gitea_url == "" def test_resolve_runner_override(self) -> None: @@ -255,17 +215,13 @@ class TestRunnerManager: "host": "10.0.0.1", "user": "ubuntu", "key": "/key", - "mode": "docker", "gitea_url": "https://git.example.com", } manager = RunnerManager(registry=mock_registry) - host, user, key, mode, gitea_url = manager._resolve_runner( - "r1", host="10.0.0.2", user="root", key="/new", mode="binary" - ) + host, user, key, gitea_url = manager._resolve_runner("r1", host="10.0.0.2", user="root", key="/new") assert host == "10.0.0.2" assert user == "root" assert key == "/new" - assert mode == "binary" assert gitea_url == "https://git.example.com" def test_resolve_runner_not_found(self) -> None: @@ -277,7 +233,7 @@ class TestRunnerManager: def test_start(self) -> None: mock_registry = MagicMock() - mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"} + mock_registry.get.return_value = {"host": "host", "user": "user", "key": None} manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() manager._executor = mock_executor @@ -287,22 +243,20 @@ class TestRunnerManager: cmd_str = " ".join(cmd) assert "start-runner.yml" in cmd_str assert "runner_name=r1" in cmd_str - assert "runner_mode=docker" in cmd_str assert "Starting Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"] def test_start_with_override(self) -> None: mock_registry = MagicMock() - mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"} + mock_registry.get.return_value = {"host": "host", "user": "user", "key": None} manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() manager._executor = mock_executor - manager.start("r1", host="newhost", user="newuser", mode="binary") + manager.start("r1", host="newhost", user="newuser") cmd = mock_executor.run.call_args.args[0] cmd_str = " ".join(cmd) assert "newhost," in cmd_str assert "newuser" in cmd_str - assert "runner_mode=binary" in cmd_str def test_start_no_registry_with_explicit(self) -> None: mock_registry = MagicMock() @@ -311,14 +265,14 @@ class TestRunnerManager: mock_executor = MagicMock() manager._executor = mock_executor - manager.start("r1", host="host", user="user", mode="docker") + manager.start("r1", host="host", user="user") cmd = mock_executor.run.call_args.args[0] cmd_str = " ".join(cmd) assert "start-runner.yml" in cmd_str def test_stop(self) -> None: mock_registry = MagicMock() - mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"} + mock_registry.get.return_value = {"host": "host", "user": "user", "key": None} manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() manager._executor = mock_executor @@ -332,7 +286,7 @@ class TestRunnerManager: def test_enable(self) -> None: mock_registry = MagicMock() - mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"} + mock_registry.get.return_value = {"host": "host", "user": "user", "key": None} manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() manager._executor = mock_executor @@ -350,7 +304,6 @@ class TestRunnerManager: "host": "host", "user": "user", "key": None, - "mode": "docker", "gitea_url": "", } manager = RunnerManager(registry=mock_registry) @@ -364,7 +317,6 @@ class TestRunnerManager: assert "runner_name=r1" in cmd_str assert "registration_token=tok" in cmd_str assert "gitea_url=https://git.example.com" in cmd_str - assert "runner_mode=docker" in cmd_str assert "Disabling Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"] def test_disable_uses_registry_gitea_url(self) -> None: @@ -373,7 +325,6 @@ class TestRunnerManager: "host": "host", "user": "user", "key": None, - "mode": "docker", "gitea_url": "https://registry.example.com", } manager = RunnerManager(registry=mock_registry) @@ -401,7 +352,7 @@ class TestRunnerManager: def test_status(self) -> None: mock_registry = MagicMock() - mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "binary"} + mock_registry.get.return_value = {"host": "host", "user": "user", "key": None} manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() manager._executor = mock_executor @@ -411,7 +362,6 @@ class TestRunnerManager: cmd_str = " ".join(cmd) assert "status-runner.yml" in cmd_str assert "runner_name=r1" in cmd_str - assert "runner_mode=binary" in cmd_str assert "Checking status of Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"] def test_remove(self) -> None: @@ -420,7 +370,6 @@ class TestRunnerManager: "host": "host", "user": "user", "key": None, - "mode": "docker", "gitea_url": "", } manager = RunnerManager(registry=mock_registry) @@ -434,7 +383,6 @@ class TestRunnerManager: assert "runner_name=r1" in cmd_str assert "registration_token=tok" in cmd_str assert "gitea_url=https://git.example.com" in cmd_str - assert "runner_mode=docker" in cmd_str assert "Removing Gitea Runner r1 from host" in mock_executor.run.call_args.kwargs["description"] mock_registry.remove.assert_called_once_with("r1") @@ -470,7 +418,6 @@ class TestRunnerManager: "host": "host", "user": "user", "key": None, - "mode": "docker", "gitea_url": "", } manager = RunnerManager(registry=mock_registry) @@ -493,14 +440,14 @@ class TestRunnerManager: mock_executor.run.assert_not_called() mock_registry.remove.assert_called_once_with("r1") - def test_list_runners_docker(self) -> None: + def test_list_runners(self) -> None: mock_registry = MagicMock() mock_registry.list.return_value = { - "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key", "mode": "docker"}, + "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key"}, } manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() - mock_executor.run_ad_hoc.return_value = "running" + mock_executor.run_ad_hoc.return_value = "active" manager._executor = mock_executor runners = manager.list_runners() @@ -513,44 +460,16 @@ class TestRunnerManager: "ubuntu", "/key", "shell", - ( - "docker inspect gitea-runner-r1 2>/dev/null | " - "python3 -c \"import sys,json; print(json.load(sys.stdin)[0]['State']['Status'])\" " - "2>/dev/null || echo unknown" - ), + "sudo -u grm-r1 systemctl --user is-active gitea-runner 2>/dev/null", become=True, ask_become_pass=True, check=False, ) - def test_list_runners_binary(self) -> None: + def test_list_runners_exception(self) -> None: mock_registry = MagicMock() mock_registry.list.return_value = { - "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key", "mode": "binary"}, - } - manager = RunnerManager(registry=mock_registry) - mock_executor = MagicMock() - mock_executor.run_ad_hoc.return_value = "active" - manager._executor = mock_executor - - runners = manager.list_runners() - assert len(runners) == 1 - assert runners[0]["status"] == "active" - mock_executor.run_ad_hoc.assert_called_once_with( - "10.0.0.1", - "ubuntu", - "/key", - "shell", - "systemctl is-active gitea-runner@r1 2>/dev/null", - become=True, - ask_become_pass=True, - check=False, - ) - - def test_list_runners_binary_exception(self) -> None: - mock_registry = MagicMock() - mock_registry.list.return_value = { - "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "binary"}, + "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None}, } manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() @@ -569,7 +488,7 @@ class TestRunnerManager: def test_list_runners_without_labels(self) -> None: mock_registry = MagicMock() mock_registry.list.return_value = { - "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"}, + "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None}, } manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() @@ -579,38 +498,23 @@ class TestRunnerManager: runners = manager.list_runners() assert runners[0]["labels"] == "" - def test_list_runners_docker_exited(self) -> None: + def test_list_runners_inactive(self) -> None: mock_registry = MagicMock() mock_registry.list.return_value = { - "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"}, + "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None}, } manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() - mock_executor.run_ad_hoc.return_value = "exited" + mock_executor.run_ad_hoc.return_value = "inactive" manager._executor = mock_executor runners = manager.list_runners() assert runners[0]["status"] == "inactive" - def test_list_runners_docker_fallback_to_host_container(self) -> None: - mock_registry = MagicMock() - mock_registry.list.return_value = { - "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"}, - } - manager = RunnerManager(registry=mock_registry) - mock_executor = MagicMock() - # First call (name-based) returns unknown, second (host-based) returns running - mock_executor.run_ad_hoc.side_effect = ["unknown", "running"] - manager._executor = mock_executor - - runners = manager.list_runners() - assert runners[0]["status"] == "active" - assert mock_executor.run_ad_hoc.call_count == 2 - def test_list_runners_unknown_status(self) -> None: mock_registry = MagicMock() mock_registry.list.return_value = { - "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"}, + "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None}, } manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() @@ -620,25 +524,10 @@ class TestRunnerManager: runners = manager.list_runners() assert runners[0]["status"] == "unknown" - def test_list_runners_docker_first_command_falls_back(self) -> None: - """When the first docker inspect raises, the loop continues to the next fallback.""" - mock_registry = MagicMock() - mock_registry.list.return_value = { - "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"}, - } - manager = RunnerManager(registry=mock_registry) - mock_executor = MagicMock() - mock_executor.run_ad_hoc.side_effect = [AnsibleError("ssh fail"), "running"] - manager._executor = mock_executor - - runners = manager.list_runners() - assert runners[0]["status"] == "active" - assert mock_executor.run_ad_hoc.call_count == 2 - def test_list_runners_ad_hoc_error(self) -> None: mock_registry = MagicMock() mock_registry.list.return_value = { - "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"}, + "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None}, } manager = RunnerManager(registry=mock_registry) mock_executor = MagicMock() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..f428892 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1301 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform == 'linux2'", + "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'linux2'", + "python_full_version < '3.14' and sys_platform == 'linux'", + "python_full_version < '3.14' and sys_platform == 'linux2'", + "python_full_version < '3.14' and sys_platform != 'linux' and sys_platform != 'linux2'", +] + +[[package]] +name = "ansible" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ansible-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/97/3aeeb9d199fd0f931452adebcf5336c01c739b1de2d4bada0744ff5d18e1/ansible-14.1.0.tar.gz", hash = "sha256:1ae7918469ed897ff716857ed98d1f5d5fce1710f6a42f601f0715548b67bf00", size = 49560987, upload-time = "2026-06-18T20:38:03.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/ba/2e7f2ef6102e79597147bba317c1648eeb92c10bd14163442ebcab9ac75b/ansible-14.1.0-py3-none-any.whl", hash = "sha256:13b7d9ce3c5b62f49b2be5cd36802adb9b1ee9f624e8db2e84b47cbfebf69cf2", size = 49543455, upload-time = "2026-06-18T20:37:56.086Z" }, +] + +[[package]] +name = "ansible-compat" +version = "26.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ansible-core" }, + { name = "jsonschema" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "subprocess-tee" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/42/53ba59f8116e716ec0af8dc47ef65b15f5e3bc28131e8e6cbe87e50203f5/ansible_compat-26.3.0.tar.gz", hash = "sha256:15f0ea5ff6fcc5587b6b11a4a436fdeefd0fd4a46afe92d4e483c28370082ae0", size = 216754, upload-time = "2026-03-31T15:47:58.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/a1/3cf5e81f192a2ae4086f80d37d80e257c7552cd7f319b0160c78251bbfea/ansible_compat-26.3.0-py3-none-any.whl", hash = "sha256:4fb48f324383033cf24df5ccb63bca4da766cb4ebbd4bc9dbad91b108f512a73", size = 27723, upload-time = "2026-03-31T15:47:56.534Z" }, +] + +[[package]] +name = "ansible-core" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "resolvelib" }, +] +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "ansible-lint" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ansible-compat" }, + { name = "ansible-core" }, + { name = "black" }, + { name = "cffi" }, + { name = "cryptography" }, + { name = "distro" }, + { name = "filelock" }, + { name = "jsonschema" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "ruamel-yaml" }, + { name = "ruamel-yaml-clib", marker = "python_full_version < '3.14'" }, + { name = "subprocess-tee" }, + { name = "wcmatch" }, + { name = "yamllint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/39/54f7cc264f7f02c635af786c4a57da4ab993865140f90e031cdec39dd514/ansible_lint-26.4.0.tar.gz", hash = "sha256:29e0438f8af685a4fec96f9ea3404a3beb50d2911bc8df43f283256954ceea5b", size = 736853, upload-time = "2026-04-01T14:41:02.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/97/803cde1dba6c5cc24f1401b37df8404a2793bd26dc3f11c0a9722109b859/ansible_lint-26.4.0-py3-none-any.whl", hash = "sha256:f33c4823544a5a8e5e36614866e111d1a929eeffee5e84481ede10d524a9d6d6", size = 330939, upload-time = "2026-04-01T14:41:00.083Z" }, +] + +[[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" } +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" }, +] + +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + +[[package]] +name = "black" +version = "26.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, + { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" }, + { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" }, + { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" }, + { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" }, + { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" }, + { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" }, + { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" }, + { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" }, + { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" }, + { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" }, + { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" }, + { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" }, + { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" }, + { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" }, + { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" }, + { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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 = "distlib" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + +[[package]] +name = "enrich" +version = "1.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/77/cb9b3d6f2e2e5f8104e907ea4c4d575267238f52c51cf9f864b865a99710/enrich-1.2.7.tar.gz", hash = "sha256:0a2ab0d2931dff8947012602d1234d2a3ee002d9a355b5d70be6bf5466008893", size = 16918, upload-time = "2022-01-10T15:30:33.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/67/aecd1d435dbbdcea21a197d708e9ff0bcc7306c2847c6c87cc1a91e2cca4/enrich-1.2.7-py3-none-any.whl", hash = "sha256:f29b2c8c124b4dbd7c975ab5c3568f6c7a47938ea3b7d2106c8a3bd346545e4f", size = 8717, upload-time = "2022-01-10T15:30:32.723Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, +] + +[[package]] +name = "gitea-runner-manager" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "ansible" }, + { name = "click" }, + { name = "python-dotenv" }, + { name = "requests" }, +] + +[package.optional-dependencies] +dev = [ + { name = "ansible-lint" }, + { name = "bandit" }, + { name = "molecule" }, + { name = "molecule-docker" }, + { name = "pre-commit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "ansible", specifier = ">=14.0.0" }, + { name = "ansible-lint", marker = "extra == 'dev'", specifier = ">=26.4.0" }, + { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.8.2" }, + { name = "click", specifier = ">=8.4.1" }, + { name = "molecule", marker = "extra == 'dev'", specifier = ">=26.4.0" }, + { name = "molecule-docker", marker = "extra == 'dev'", specifier = ">=2.1.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.6.0" }, + { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.410" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.1.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, + { name = "requests", specifier = ">=2.34.2" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.17" }, +] +provides-extras = ["dev"] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "molecule" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ansible-compat" }, + { name = "ansible-core" }, + { name = "click" }, + { name = "enrich" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/e0/f671b5b0742a60b6067cbe9e57720b600ded5e0fcabb837663dd21b57568/molecule-26.4.0.tar.gz", hash = "sha256:12e4c905079f67628ae765506c697d2b8a744a65f2d4cbf5a3b22cb09d0dafc4", size = 4699013, upload-time = "2026-04-06T09:01:33.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/11/9f8322de3674b6ac12d0d203d18f81eb9262af19d2b8122c08b11821af97/molecule-26.4.0-py3-none-any.whl", hash = "sha256:b231f4955f95240a8e2b8194c086d0b5e8ab998032a9411eafe85429442a3bd5", size = 158898, upload-time = "2026-04-06T09:01:31.287Z" }, +] + +[[package]] +name = "molecule-docker" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "molecule" }, + { name = "requests" }, + { name = "selinux", marker = "sys_platform == 'linux' or sys_platform == 'linux2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/39/73b6222cf3bfef0f07a8cf03e558bfa723b2ff706664fa26393d6b628e28/molecule-docker-2.1.0.tar.gz", hash = "sha256:195b97673cbc2335cfa6810816de5cbf807507bf350a9d16ca98b224b1647145", size = 22153, upload-time = "2022-09-29T10:28:40.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/c0/a5df7161d7085b9b53e05df3ef27f425d3a7fc6ad03e6723ea8f1cac1f1b/molecule_docker-2.1.0-py3-none-any.whl", hash = "sha256:d439b075789be700b6594ed73f3254e2a25ed61dcf312d80ab6e718d13bf150e", size = 18351, upload-time = "2022-09-29T10:28:39.023Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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 = "pyright" +version = "1.1.410" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/53/e4d8ea1391bd4355231be6f91bf239479aa0014260ed3fb5526eeb12a1f2/pyright-1.1.410.tar.gz", hash = "sha256:07a073b8ba6749826773c1269773efa11b93440d9a6aa60419d9a3172d6dc488", size = 4062013, upload-time = "2026-06-01T17:35:48.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/33/288b5868fa00846dacf249633719d747893e54aebd196b9968ac1878a5d3/pyright-1.1.410-py3-none-any.whl", hash = "sha256:5e961bed37cacf96b3f7cd7b1da39b350a9239aa2e69138d0e88f728cfaf296c", size = 6082448, upload-time = "2026-06-01T17:35:46.387Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "resolvelib" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "rpds-py" +version = "2026.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, + { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, + { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, + { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, + { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" }, + { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, + { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, + { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, + { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, + { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, + { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, + { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, + { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, + { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, + { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, + { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, + { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, + { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, + { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, + { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "ruamel-yaml-clib" +version = "0.2.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, + { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, + { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, + { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, + { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, + { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, + { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" }, + { url = "https://files.pythonhosted.org/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" }, + { url = "https://files.pythonhosted.org/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" }, + { url = "https://files.pythonhosted.org/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" }, + { url = "https://files.pythonhosted.org/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" }, + { url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" }, +] + +[[package]] +name = "selinux" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distro", marker = "sys_platform == 'linux' or sys_platform == 'linux2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/07/51acd62e1e15e1172d46f7e32faf138725b147f8c08dbf2d512159d7a310/selinux-0.3.0.tar.gz", hash = "sha256:2a88b337ac46ad0f06f557b2806c3df62421972f766673dd8bf26732fb75a9ea", size = 13479, upload-time = "2022-12-24T16:42:13.163Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/a7/ef33f8e5818ed6333cf5cb6f85f4f604e13d37d1bae4671e10d6f627f6d6/selinux-0.3.0-py2.py3-none-any.whl", hash = "sha256:ecf7add45c939e9dda682c390a2cd0a845c94a4793a2cce9e8870d4ee9501f99", size = 4160, upload-time = "2022-12-24T16:42:11.745Z" }, +] + +[[package]] +name = "stevedore" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/88/35e4d27d9177d7df76d060e0a18f69c6c5794c96960c94042e20a12c8ba2/stevedore-5.8.0.tar.gz", hash = "sha256:b49867b32ca3016e94100e68dbf26e72aa7b8708d0a3f73c08aeb220370ac715", size = 514710, upload-time = "2026-05-18T09:15:27.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/ac/19f9941c74add59d17694930ec8105d5eddeee4ce56dd8632b765ca16d6c/stevedore-5.8.0-py3-none-any.whl", hash = "sha256:88eede9e66ca80e34085b9174e2327da2c61ac91f24f70e41c3ad76e4bb4872b", size = 54553, upload-time = "2026-05-18T09:15:25.82Z" }, +] + +[[package]] +name = "subprocess-tee" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/22/991efbf35bc811dfe7edcd749253f0931d2d4838cf55176132633e1c82a7/subprocess_tee-0.4.2.tar.gz", hash = "sha256:91b2b4da3aae9a7088d84acaf2ea0abee3f4fd9c0d2eae69a9b9122a71476590", size = 14951, upload-time = "2024-06-17T19:51:51.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ab/e3a3be062cd544b2803760ff707dee38f0b1cb5685b2446de0ec19be28d9/subprocess_tee-0.4.2-py3-none-any.whl", hash = "sha256:21942e976715af4a19a526918adb03a8a27a8edab959f2d075b777e3d78f532d", size = 5249, upload-time = "2024-06-17T19:51:15.949Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + +[[package]] +name = "yamllint" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathspec" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/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/05/92/aed08e68de6e6a3d7c2328ce7388072cd6affc26e2917197430b646aed02/yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220", size = 68940, upload-time = "2026-01-13T07:47:51.343Z" }, +]