Compare commits

..

121 Commits

Author SHA1 Message Date
av 7d711425fd Migration: update steps
Linting / YAML Lint (push) Has been cancelled
Linting / Ansible Lint (push) Has been cancelled
2026-05-22 21:06:16 +03:00
av e03a4c417d Migration: fix tags and docker registry vars 2026-05-22 21:01:11 +03:00
av 10e1e8187b Migration: fix vars in playbooks
Linting / YAML Lint (push) Has been cancelled
Linting / Ansible Lint (push) Has been cancelled
2026-05-22 20:52:47 +03:00
av 1ce168655d Migration: application directory as parameter 2026-05-22 20:45:52 +03:00
av fe024b3b12 Migration: fix inventory hardcode in tasks.py
Linting / YAML Lint (push) Has been cancelled
Linting / Ansible Lint (push) Has been cancelled
2026-05-22 20:21:36 +03:00
av 8378f0edb0 Migration: expose some public vars 2026-05-22 20:18:45 +03:00
av 48737c1b6d Migration: optional external hdd mount 2026-05-22 20:17:12 +03:00
av 600a30ec11 Migration: add migration log 2026-05-22 20:11:23 +03:00
av 670947fcdf Migration: login to docker registry with oauth key 2026-05-22 20:11:10 +03:00
av 3545905cbd Migration: add draft for timeweb migration 2026-05-22 19:55:54 +03:00
av 893996f0c9 Docs: add docs and drafts 2026-05-22 19:13:05 +03:00
av 4a5db6e2bc Dozzle: update to 10.6.0
Linting / YAML Lint (push) Has been cancelled
Linting / Ansible Lint (push) Has been cancelled
2026-05-22 19:04:45 +03:00
av ca6875eaad Dozzle: update to 10.5.3
Linting / YAML Lint (push) Has been cancelled
Linting / Ansible Lint (push) Has been cancelled
2026-05-12 15:46:56 +03:00
av dbf0aa255a Gramps: update to 26.5.1 2026-05-12 15:46:22 +03:00
av bc6fff68bb Outline: update to 1.7.1
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 33s
2026-05-04 12:24:02 +03:00
av 512f31b350 Tuwunel: update to 1.6.1 2026-05-04 12:23:44 +03:00
av d25a28c611 Netdata: add alerts for cpu, ram, disks
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 31s
2026-05-01 13:58:51 +03:00
av 472c7a984f Homepage: simplify deploy
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 30s
2026-05-01 11:15:39 +03:00
av df3a37e610 Backups: backup to home server storage
Linting / YAML Lint (push) Successful in 41s
Linting / Ansible Lint (push) Failing after 1m4s
2026-05-01 10:49:27 +03:00
av 8efab2002f Rename all j2 files to templates
Not according to convention, but it reads better.
2026-05-01 10:01:26 +03:00
av 6edb72077a Homepage: release homepage-nginx:89c2a66-1777484248 2026-04-29 20:37:38 +03:00
av 07eacad003 Netdata: update to 2.10.3
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 29s
2026-04-29 20:26:23 +03:00
av 3b1736534d GoAccess: combine host and path in reports 2026-04-29 20:26:05 +03:00
av 4d92b3bd3e GoAccess: add for caddy logs monitoring
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 33s
2026-04-29 20:10:08 +03:00
av 27834c6711 Dozzle: update to 10.5.0
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 29s
2026-04-27 09:39:24 +03:00
av 89f46566c8 Memos: update to 0.28.0 2026-04-27 09:39:01 +03:00
av 7f1809b4ca Netdata: update to 2.10.2
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 34s
2026-04-26 10:41:54 +03:00
av 6784381833 Outline: update to 1.7.0 2026-04-26 10:41:40 +03:00
av 7c42acf893 Gitea: update to 1.26.1 2026-04-26 10:41:24 +03:00
av 452f7973a9 Tuwunel: install matrix server
Linting / YAML Lint (push) Successful in 13s
Linting / Ansible Lint (push) Failing after 34s
2026-04-20 21:39:49 +03:00
av 303aefb75f Gitea: update to 1.26.0
Linting / YAML Lint (push) Successful in 12s
Linting / Ansible Lint (push) Failing after 35s
2026-04-19 13:54:55 +03:00
av 22307d81c9 Memos: update to 0.27.1 2026-04-19 13:54:41 +03:00
av cc811f954d Dozzle: update to 10.4.1 2026-04-19 13:54:25 +03:00
av f17c4ac227 backup: error count in title 2026-04-12 18:05:49 +03:00
av 25d20df5a9 backup: notifications as html 2026-04-12 18:03:25 +03:00
av 7e1a8e2e99 backup: method ordering 2026-04-12 18:00:32 +03:00
av b90b87caa1 backup: sort apps 2026-04-12 17:58:03 +03:00
av 75ce60d8a0 backup: extend application with scripts and backup paths 2026-04-12 17:53:17 +03:00
av 0aa34efd00 backup: add application finder class 2026-04-12 17:42:43 +03:00
av b7a18f1296 apps: updates 2026-04-12 17:33:59 +03:00
av 6bfb362b20 backups: notifications to email
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 31s
2026-04-04 13:16:54 +03:00
av 5f619eaccc backups: use apprise for notifications
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 29s
2026-04-04 11:30:53 +03:00
av 362d6d8710 apprise: changed to simple stateful setup 2026-04-04 11:25:27 +03:00
av 5e6df110c8 apprise: only one worker for small memory consumption 2026-04-04 10:54:27 +03:00
av a0543e13f4 Add apprise application for notifications 2026-04-04 10:50:10 +03:00
av 41fe116dd7 Remove old public key 2026-04-04 10:49:53 +03:00
av e34f8505a2 Add login as app task
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 27s
2026-04-04 10:14:28 +03:00
av 53f43264cc Remove task link from readme 2026-04-04 09:52:54 +03:00
av c1f5eaeca0 Add remembos playbook to all-applications 2026-04-04 09:51:19 +03:00
av fb9c754461 Rewrite AGENTS.md 2026-04-04 09:48:37 +03:00
av b7495e1d6b wakapi: upgrade to 2.17.3
Linting / YAML Lint (push) Successful in 41s
Linting / Ansible Lint (push) Failing after 1m1s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:13:01 +03:00
av 8ab5e89851 gramps: upgrade to 26.4.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:12:46 +03:00
av 7f0a2a8eb6 dozzle: upgrade to v10.2.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:12:28 +03:00
av c686b292dc Update applications
Linting / YAML Lint (push) Successful in 11s
Linting / Ansible Lint (push) Failing after 33s
Authelia: 4.39.16
Dozzle: 10.1.2
Gitea: 1.25.5
Outline: 1.6.1
2026-03-23 19:31:59 +03:00
av fb87cf77b2 Update applications
Linting / YAML Lint (push) Successful in 11s
Linting / Ansible Lint (push) Failing after 32s
2026-03-11 09:51:23 +03:00
av 4e467d0f9b Homepage: release homepage-nginx:531c1cd-1772885069 2026-03-07 15:04:32 +03:00
av b44ec03b7b Homepage: release homepage-nginx:ec0ae72-1772876870 2026-03-07 12:47:54 +03:00
av ebfcc1d3ab Homepage: release homepage-nginx:8beca48-1772876712 2026-03-07 12:45:16 +03:00
av 9183bbc58b Homepage: release homepage-nginx:8beca48-1772875094 2026-03-07 12:18:18 +03:00
av 6005cbaae8 Homepage: release homepage-nginx:7a02272-1772874721 2026-03-07 12:12:04 +03:00
av b839b2787f Homepage: release homepage-nginx:7392ce4-1772873114 2026-03-07 11:45:17 +03:00
av c714971595 Homepage: release homepage-nginx:9db0e5d-1772872682 2026-03-07 11:38:05 +03:00
av 7c02f5f22a Homepage: release homepage-nginx:9db0e5d-1772872520 2026-03-07 11:35:24 +03:00
av c9440c8d50 Homepage: release homepage-nginx:0e571f3-1772872290 2026-03-07 11:31:33 +03:00
av b5ebfeee39 Homepage: release homepage-nginx:0e571f3-1772872254 2026-03-07 11:31:01 +03:00
av 96f5e11bbc Homepage: release homepage-nginx:0e571f3-1772872217 2026-03-07 11:30:20 +03:00
av c16131e773 Homepage: release homepage-nginx:0e571f3-1772872152 2026-03-07 11:29:15 +03:00
av 6350a1112d Add zellij
Linting / YAML Lint (push) Successful in 11s
Linting / Ansible Lint (push) Failing after 35s
2026-03-04 20:17:35 +03:00
av f3b888853e Remove old s3 connections for gramps and outline
Linting / YAML Lint (push) Successful in 41s
Linting / Ansible Lint (push) Failing after 1m0s
2026-03-01 12:24:14 +03:00
av 2709547958 Dozzle: update to 10.0.5
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 31s
2026-02-27 10:32:13 +03:00
av 988730c798 Memos: update to 0.26.2
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 32s
2026-02-23 17:13:45 +03:00
av ae3f925777 Remove Taskfile.yml 2026-02-22 20:55:24 +03:00
av b13cc65a14 Improve pl task
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 28s
2026-02-22 20:55:01 +03:00
av b793b7806b Migrate from task to invoke 2026-02-22 18:41:29 +03:00
av d6be9fbfb8 Add invoke as task runner, ruff and fix mypy errors 2026-02-22 18:33:59 +03:00
av 527ca62cb2 Remembos: update to 0.1.5
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 31s
2026-02-22 12:36:24 +03:00
av c492e6f697 Dozzle: update to 10.0.4 2026-02-22 12:36:05 +03:00
av d46b44bc70 Transcriber: update speech kit api key
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 28s
2026-02-18 10:52:02 +03:00
av e0ffb8636d Dozzle: update to 10.0.2
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 31s
2026-02-18 09:52:04 +03:00
av 5ccee8e0a1 Outline: update to 1.5.0 2026-02-18 09:51:43 +03:00
av a66b5fdc3d Netdada: update to 2.9.0 2026-02-18 09:51:25 +03:00
av dad43879b2 Remembos: update to 0.1.4
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 32s
2026-02-13 10:22:26 +03:00
av 3880aefdfd Remembos: update to 0.1.1 2026-02-12 20:43:50 +03:00
av 254edf1e45 Remembos: install app 2026-02-12 20:27:10 +03:00
av e427422253 Dozzle: update to 10.0.0
Linting / YAML Lint (push) Successful in 11s
Linting / Ansible Lint (push) Failing after 31s
2026-02-11 09:35:42 +03:00
av 516497c8fd Memos: update to 0.26.1
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 33s
2026-02-08 19:59:42 +03:00
av 670d830e03 Calibre: update to 0.6.26
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 32s
2026-02-07 10:29:13 +03:00
av b47826feb4 Memos: canary version with bug fixes 2026-02-07 10:28:43 +03:00
av 5ce2f1fbd4 Memos: update to 0.26.0
Linting / YAML Lint (push) Successful in 15s
Linting / Ansible Lint (push) Failing after 51s
2026-02-02 11:00:45 +03:00
av e39981eee2 Gramps: update to 26.1.0
Linting / YAML Lint (push) Successful in 12s
Linting / Ansible Lint (push) Failing after 34s
2026-01-28 09:28:44 +03:00
av 83bfba2180 Outline: update to 1.4.0 2026-01-28 09:28:26 +03:00
av b42dd429fd Dozzle: update to 9.0.3 2026-01-28 09:28:09 +03:00
av a056e8662d Gitea: upgrade to 1.25.4
Linting / YAML Lint (push) Successful in 11s
Linting / Ansible Lint (push) Failing after 32s
2026-01-22 09:28:38 +03:00
av 4a693470fc Dozzle: upgrade to 9.0.2 2026-01-22 09:28:04 +03:00
av ab9ac67b2e Outline: upgrade to 1.3.0
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 28s
2026-01-18 10:26:18 +03:00
av 8728eb0203 Improve docs
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 29s
2026-01-18 10:02:44 +03:00
av 926f4ea135 Calibre: add application
Linting / YAML Lint (push) Successful in 11s
Linting / Ansible Lint (push) Failing after 33s
Remove Kavita
2026-01-18 09:56:11 +03:00
av 7fb65caf66 Kavita: add application 2026-01-15 13:21:25 +03:00
av d5d8bb71d8 Netdata: upgrade to 2.8.5
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 31s
2026-01-14 09:50:25 +03:00
av 07443e4b2e Dozzle: upgrade to 9.0.1
Linting / YAML Lint (push) Successful in 11s
Linting / Ansible Lint (push) Failing after 31s
2026-01-09 10:46:08 +03:00
av 396c2048ae Outline: upgrade to 1.2.0
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 31s
2026-01-07 12:13:19 +03:00
av 62c47cc5d7 Dozzle: upgrade to 9.0.0 2026-01-07 10:18:29 +03:00
av a44e3d6766 add ufw settings
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Failing after 46s
2026-01-05 21:00:32 +03:00
av 2e56cc97d9 remove production profile
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 28s
2026-01-02 20:33:48 +03:00
av 1a98aa504c increase verbosity
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Failing after 28s
2026-01-02 20:30:45 +03:00
av 47d464109b increase verbosity 2026-01-02 20:24:19 +03:00
av 6358e3795c increase verbosity
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Successful in 17s
2026-01-02 20:16:18 +03:00
av d0ae59ce36 fix ansible lint profile
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Successful in 17s
2026-01-02 20:12:33 +03:00
av d169d7996f update requirements
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Successful in 17s
2026-01-02 19:57:27 +03:00
av f80a1008c7 fix ansible lint errors 2026-01-02 19:44:35 +03:00
av af5b00d62d fix yamllint errors 2026-01-02 19:28:29 +03:00
av 19ae4632ea add yamllint to lefthook 2026-01-02 19:25:50 +03:00
av 188756501a change task commands to use 'uv run' 2026-01-02 19:20:34 +03:00
av 4cf3b52f2e change task commands to use 'uv run' 2026-01-02 19:19:00 +03:00
av c36752a16f add venv with uv 2026-01-02 19:18:07 +03:00
av a2f7e90f89 Rearrange git hooks 2025-12-29 20:19:18 +03:00
av 2ea3b1b166 Wakapi: upgrade to 2.17.0
Linting / Ansible Lint (push) Failing after 33s
Linting / YAML Lint (push) Failing after 35s
2025-12-29 20:17:19 +03:00
av e5c1e19e5e Backups: fix host name
Linting / YAML Lint (push) Failing after 8s
Linting / Ansible Lint (push) Successful in 16s
2025-12-21 14:18:05 +03:00
av 8439ab3693 Outline: migrate assets to local storage 2025-12-21 14:17:47 +03:00
av dcb09e349b Secrets: update outline secrets before migration to local storage 2025-12-21 14:17:30 +03:00
av a6781b4f64 Gramps: upgrade to 25.12.0
Linting / YAML Lint (push) Failing after 8s
Linting / Ansible Lint (push) Successful in 16s
2025-12-21 12:24:11 +03:00
101 changed files with 4439 additions and 872 deletions
+3
View File
@@ -1,6 +1,9 @@
---
exclude_paths:
- ".ansible/"
- ".crush/"
- ".gitea/"
- ".venv/"
- ".vscode/"
- "galaxy.roles/"
- "Taskfile.yml"
+1 -1
View File
@@ -47,4 +47,4 @@ jobs:
fi
- name: Run ansible-lint
run: ansible-lint .
run: ansible-lint -vv
+1
View File
@@ -1,6 +1,7 @@
/.ansible
/.idea
/.vagrant
/.venv
/.vscode
/galaxy.roles/
+1
View File
@@ -0,0 +1 @@
3.12
+3
View File
@@ -2,6 +2,9 @@ extends: default
ignore:
- ".ansible/"
- ".crush/"
- ".venv/"
- ".vscode/"
- "galaxy.roles/"
rules:
+127 -59
View File
@@ -1,69 +1,137 @@
# AGENTS GUIDE
## Overview
Ansible-based server automation for personal services. Playbooks provision Dockerized apps (e.g., gitea, authelia, homepage, miniflux, wakapi, memos) via per-app users, Caddy proxy, and Yandex Docker Registry. Secrets are managed with Ansible Vault.
## Обзор
## Project Layout
- Playbooks: `playbook-*.yml` (per service), `playbook-all-*.yml` for grouped actions.
- Inventory: `production.yml` (ungrouped host `server`).
- Variables: `vars/*.yml` (app configs, images), secrets in `vars/secrets.yml` (vault-encrypted).
- Roles: custom roles under `roles/` (e.g., `eget`, `owner`, `secrets`) plus galaxy roles fetched to `galaxy.roles/`.
- Files/templates: service docker-compose and backup templates under `files/`, shared templates under `templates/`.
- Scripts: helper Python scripts in `scripts/` (SMTP utilities) and `files/backups/backup-all.py`.
- CI: `.gitea/workflows/lint.yml` runs yamllint and ansible-lint.
- Hooks: `lefthook.yml` references local hooks in `/home/av/projects/private/git-hooks` (gitleaks, vault check).
- Formatting: `.editorconfig` enforces LF, trailing newline, 4-space indent; YAML/Jinja use 2-space indent.
Ansible-проект для автоматизации личного сервера. Плейбуки разворачивают докеризированные приложения (gitea, authelia, miniflux, wakapi, memos, outline, gramps, calibre, wanderer, remembos, transcriber и др.) через выделенных системных пользователей, Caddy-прокси и Yandex Docker Registry. Секреты управляются через Ansible Vault.
## Setup
- Copy vault password sample: `cp ansible-vault-password-file.dist ansible-vault-password-file` (needed for ansible and CI).
- Install galaxy roles: `ansible-galaxy role install --role-file requirements.yml --force` (or `task install-roles`).
- Ensure `yq`, `task`, `ansible` installed per README requirements.
## Структура проекта
## Tasks (taskfile)
- `task install-roles` — install galaxy roles into `galaxy.roles/`.
- `task ssh` — SSH to target using inventory (`production.yml`).
- `task btop` — run `btop` on remote.
- `task encrypt|decrypt -- <files>` — ansible-vault helpers.
- Authelia helpers:
- `task authelia-cli -- <args>` — run authelia CLI in docker.
- `task authelia-validate-config` — render `files/authelia/configuration.template.yml` with secrets and validate via authelia docker image.
- `task authelia-gen-random-string LEN=64` — generate random string.
- `task authelia-gen-secret-and-hash LEN=72` — generate hashed secret.
- `task format-py-files` — run Black via docker (pyfound/black).
- `playbook-*.yml` — плейбуки по одному на сервис, `playbook-all-*.yml` для групповых запусков.
- `production.yml` — инвентарь с единственным хостом `server`.
- `vars/*.yml` — переменные приложений и образов, `vars/secrets.yml` — зашифрованные секреты (vault).
- `roles/` — кастомные роли (`eget`, `owner`, `secrets`), галактические роли в `galaxy.roles/`.
- `files/<app>/` — docker-compose шаблоны, конфиги, скрипты бэкапов для каждого сервиса.
- `templates/` — общие шаблоны (например `env.template`).
- `scripts/` — вспомогательные Python-скрипты (SMTP-утилиты для Yandex Cloud Postbox).
- `.gitea/workflows/lint.yml` — CI: yamllint + ansible-lint.
- `lefthook.yml` — pre-commit хуки (ruff, mypy, yamllint, ansible-lint, gitleaks, проверка vault).
- `tasks.py` — задачи через invoke (`inv <task>`).
- `pyproject.toml` — зависимости Python, управляются через `uv`.
## Ansible Usage
- Inventory: `production.yml` with `server` host. `ansible.cfg` points to `./ansible-vault-password-file` and `./galaxy.roles` for roles path.
- Typical deploy example (from README): `ansible-playbook -i production.yml --diff playbook-gitea.yml`.
- Per-app playbooks: `playbook-<app>.yml`; grouped runs: `playbook-all-setup.yml`, `playbook-all-applications.yml`, `playbook-upgrade.yml`, etc.
- Secrets: encrypted `vars/secrets.yml`; additional `files/<app>/secrets.yml` used for templating (e.g., Authelia). Respect `.crushignore` ignoring vault files.
- Templates: many `docker-compose.template.yml` and `*.template.sh` files under `files/*` plus shared `templates/env.j2`. Use `vars/*.yml` to supply values.
- Custom roles:
- `roles/eget`: installs `eget` tool; see defaults/vars for version/source.
- `roles/owner`: manages user/group and env template.
- `roles/secrets`: manages vault-related items.
## Настройка окружения
## Linting & CI
- Local lint configs: `.yamllint.yml`, `.ansible-lint.yml` (excludes `.ansible/`, `.gitea/`, `galaxy.roles/`, `Taskfile.yml`).
- CI (.gitea/workflows/lint.yml) installs `yamllint` and `ansible-lint` and runs `yamllint .` then `ansible-lint .`; creates dummy vault file if missing.
- Pre-commit via lefthook (local hooks path): runs `gitleaks git --staged` and secret-file vault check script.
```bash
uv sync
cp ansible-vault-password-file.dist ansible-vault-password-file
uv run ansible-galaxy install --role-file requirements.yml
```
## Coding/Templating Conventions
- Indentation: 2 spaces for YAML/Jinja (`.editorconfig`), 4 spaces default elsewhere.
- End-of-line: LF; ensure final newline.
- Template suffixes `.template.yml`, `.yml.j2`, `.template.sh` are rendered via Ansible `template` module.
- Avoid committing real secrets; `.crushignore` excludes `ansible-vault-password-file` and `*secrets.yml`.
- Service directories under `files/` hold docker-compose and backup templates; ensure per-app users and registry settings align with `vars/*.yml`.
Требуется: `uv`, `ansible`, `yq`.
## Testing/Validation
- YAML lint: `yamllint .` (CI default).
- Ansible lint: `ansible-lint .` (CI default).
- Authelia config validation: `task authelia-validate-config` (renders with secrets then validates via docker).
- Black formatting for Python helpers: `task format-py-files`.
- Python types validation with mypy: `mypy <file.py>`.
## Задачи (invoke)
## Operational Notes
- Deployments rely on `production.yml` inventory and per-app playbooks; run with `--diff` for visibility.
- Yandex Docker Registry auth helper: `files/yandex-docker-registry-auth.sh`.
- Backups: templates and scripts under `files/backups/` per service; `backup-all.py` orchestrates.
- Home network/DNS reference in README (Yandex domains).
- Ensure `ansible-vault-password-file` present for vault operations and CI.
Таск-раннер — `invoke` (файл `tasks.py`), вызывается через `inv`:
- `inv pl -- <app> [app2 ...]` — запуск плейбука (`ansible-playbook -i production.yml --diff`).
- `inv install-roles` — установка галактических ролей.
- `inv ssh` — SSH на сервер.
- `inv zj` — zellij на удалённом сервере.
- `inv btop` — btop на удалённом сервере.
- `inv encrypt -- <file>` / `inv decrypt -- <file>` — шифрование/дешифрование через ansible-vault.
- `inv authelia-cli -- <args>` — запуск Authelia CLI в docker.
- `inv authelia-validate-config` — рендер и валидация конфига Authelia через docker.
- `inv authelia-gen-random-string LEN=10` — генерация случайной строки.
- `inv authelia-gen-secret-and-hash LEN=72` — генерация секрета и его хэша (pbkdf2-sha512).
- `inv format-py-files` — форматирование Python-файлов через Black (docker).
## Плейбуки
### Системные
- `playbook-system.yml` — базовая настройка системы (apt-пакеты, безопасность, fail2ban, монтирование хранилища).
- `playbook-docker.yml` — установка Docker CE, создание сетей (web_proxy_network, monitoring_network), cron очистки образов.
- `playbook-eget.yml` — установка eget и инструментов через него (rclone, restic, resticprofile, btop, gobackup, task, dust, zellij).
- `playbook-ufw.yml` — настройка файрвола UFW (SSH/22, Gitea SSH/2222, HTTP/80, HTTPS/443).
- `playbook-upgrade.yml` — обновление системных пакетов, очистка Docker.
- `playbook-backups.yml` — настройка restic-бэкапов и оркестратора backup-all.py с cron-расписанием.
- `playbook-caddyproxy.yml` — Caddy reverse proxy.
### Приложения
- `playbook-gitea.yml` — Git-сервер.
- `playbook-authelia.yml` — аутентификация/SSO.
- `playbook-miniflux.yml` — RSS-ридер.
- `playbook-wakapi.yml` — трекинг времени.
- `playbook-memos.yml` — заметки.
- `playbook-outline.yml` — вики/база знаний.
- `playbook-homepage.yml` — дашборд (образ из Yandex Registry).
- `playbook-rssbridge.yml` — RSS-агрегатор.
- `playbook-netdata.yml` — мониторинг.
- `playbook-dozzle.yml` — просмотр Docker-логов.
- `playbook-goaccess.yml` — аналитика веб-логов Caddy в реальном времени.
- `playbook-gramps.yml` — генеалогия.
- `playbook-calibre.yml` — управление электронными книгами.
- `playbook-transcriber.yml` — транскрибация (образ из Yandex Registry).
- `playbook-wanderer.yml` — пешие маршруты.
- `playbook-remembos.yml` — интервальное повторение.
- `playbook-tuwunel.yml` — Matrix-сервер (Tuwunel) с federation-делегацией на apex-домен.
### Агрегатные и служебные
- `playbook-all-setup.yml` — системная настройка целиком (system + docker + eget + backups).
- `playbook-all-applications.yml` — деплой всех приложений.
- `playbook-homepage-registry.yml` / `playbook-transcriber-registry.yml` — загрузка образов в Yandex Registry.
- `playbook-remove-user-and-app.yml` — удаление пользователя и приложения (`--extra-vars user_name=<name>`).
## Роли
- `roles/owner` — создаёт системного пользователя/группу для приложения, настраивает SSH-ключи, переменные окружения (~/.env, ~/.bashrc).
- `roles/eget` — скачивает и устанавливает утилиту eget.
- `roles/secrets` — управляет vault-зашифрованными файлами секретов для приложений.
Галактические роли (`galaxy.roles/`): `geerlingguy.security`, `geerlingguy.docker`, `yatesr.timezone`.
## Шаблоны и переменные
- Суффиксы шаблонов: `.template.yml`, `.template.sh`, `.template.cfg`, `.template.conf`, `.template.toml`, `.template` (для файлов без естественного расширения) — рендерятся Ansible модулем `template`. Расширение оригинального формата сохраняется после `.template.` ради подсветки синтаксиса в редакторе.
- Большинство приложений определяют переменные inline в плейбуке. Отдельные файлы переменных только у homepage и transcriber (`vars/homepage.yml`, `vars/transcriber.yml` + `vars/transcriber.images.yml`).
- Общие переменные из `vars/secrets.yml`: `application_dir`, `bin_prefix`, `primary_user` и др.
- Каждое приложение: `app_name`, `app_user`, `app_owner_uid`, `app_owner_gid`, `base_dir`, `data_dir`.
## Линтинг и CI
- CI (`.gitea/workflows/lint.yml`): два параллельных job — yamllint и ansible-lint.
- Конфиги: `.yamllint.yml` (макс. длина строки 120), `.ansible-lint.yml` (профиль production, offline).
- Pre-commit хуки через lefthook:
- `ruff format` + `ruff check` — форматирование и линтинг Python.
- `mypy` — проверка типов Python.
- `yamllint` — линтинг YAML.
- `ansible-lint` — линтинг Ansible (профиль production).
- `gitleaks` — поиск секретов в staged-файлах.
- Проверка что секретные файлы зашифрованы vault.
## Соглашения по коду
- Отступы: 2 пробела для YAML/Jinja, 4 пробела в остальных файлах (`.editorconfig`).
- Окончания строк: LF, завершающий перевод строки обязателен.
- Не коммитить незашифрованные секреты; `.crushignore` исключает `ansible-vault-password-file` и `*secrets.yml`.
- Директории в `files/<app>/` содержат docker-compose и шаблоны бэкапов; пользователи и настройки реестра должны соответствовать `vars/*.yml`.
## Деплой
```bash
# Один сервис
inv pl -- gitea
# Несколько сервисов
inv pl -- gitea miniflux wakapi
# Напрямую через ansible-playbook
ansible-playbook -i production.yml --diff playbook-gitea.yml
```
## Бэкапы
- Шаблоны скриптов бэкапов в `files/<app>/` (backup.template.sh, gobackup.template.yml и др.).
- `files/backups/backup-all.py` — оркестратор, запускает все бэкапы через restic.
- Cron-расписание настраивается в `playbook-backups.yml`.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+17 -7
View File
@@ -7,24 +7,28 @@
## Требования
- [uv](https://docs.astral.sh/uv/)
- [ansible](https://docs.ansible.com/ansible/latest/getting_started/index.html)
- [task](https://taskfile.dev/)
- [yq](https://github.com/mikefarah/yq)
## Установка
```bash
$ cp ansible-vault-password-file.dist ansible-vault-password-file
$ ansible-galaxy install --role-file requirements.yml
uv sync
cp ansible-vault-password-file.dist ansible-vault-password-file
uv run ansible-galaxy install --role-file requirements.yml
```
## Структура
- Для каждого приложения создается свой пользователь (опционально).
- Для каждого приложения создается свой пользователь.
- Для доступа используется ssh-ключ.
- Безопасность осуществляется с помощью `ufw` и `fail2ban`.
- Докер используется для запуска и изоляции приложений. Для загрузки образов настраивается Yandex Docker Registry.
- Выход во внешнюю сеть через proxy server [Caddy](https://caddyserver.com/).
- Чувствительные данные в `vars/vars.yaml` зашифрованы с помощью Ansible Vault.
- Чувствительные данные в [secrets.yml](vars/secrets.yml) зашифрованы с помощью Ansible Vault.
- Для мониторинга за сервером устанавливается [netdata](https://github.com/netdata/netdata).
## Настройка DNS
@@ -33,8 +37,14 @@ $ ansible-galaxy install --role-file requirements.yml
## Деплой приложений
Деплой всех приложений через ansible:
Деплой приложения через ansible:
```bash
ansible-playbook -i production.yml --diff playbook-gitea.yml
uv run ansible-playbook ansible-playbook -i production.yml --diff playbook-gitea.yml
```
## Удаление приложения <name>
```bash
uv run ansible-playbook -i production.yml --diff playbook-remove-user-and-app.yml --extra-vars user_name=<name>
```
-80
View File
@@ -1,80 +0,0 @@
# https://taskfile.dev
version: '3'
vars:
USER_ID:
sh: 'id -u'
GROUP_ID:
sh: 'id -g'
HOSTS_FILE: 'production.yml'
REMOTE_USER:
sh: 'yq .ungrouped.hosts.server.ansible_user {{.HOSTS_FILE}}'
REMOTE_HOST:
sh: 'yq .ungrouped.hosts.server.ansible_host {{.HOSTS_FILE}}'
AUTHELIA_DOCKER: 'docker run --rm -v $PWD:/data authelia/authelia:4.39.4 authelia'
tasks:
install-roles:
cmds:
- ansible-galaxy role install --role-file requirements.yml --force
ssh:
cmds:
- ssh {{.REMOTE_USER}}@{{.REMOTE_HOST}}
btop:
cmds:
- ssh {{.REMOTE_USER}}@{{.REMOTE_HOST}} -t btop
encrypt:
cmds:
- ansible-vault encrypt {{.CLI_ARGS}}
decrypt:
cmds:
- ansible-vault decrypt {{.CLI_ARGS}}
authelia-cli:
cmds:
- "{{.AUTHELIA_DOCKER}} {{.CLI_ARGS}}"
authelia-validate-config:
vars:
DEST_FILE: "temp/configuration.yml"
cmds:
- >
ansible localhost
--module-name template
--args "src=files/authelia/configuration.template.yml dest={{.DEST_FILE}}"
--extra-vars "@vars/secrets.yml"
--extra-vars "@files/authelia/secrets.yml"
- defer: rm -f {{.DEST_FILE}}
- >
{{.AUTHELIA_DOCKER}}
validate-config --config /data/{{.DEST_FILE}}
authelia-gen-random-string:
summary: |
Generate random string.
Usage example:
task authelia-gen-random-string LEN=64
vars:
LEN: '{{ .LEN | default 10 }}'
cmds:
- >
{{.AUTHELIA_DOCKER}}
crypto rand --length {{.LEN}} --charset alphanumeric
authelia-gen-secret-and-hash:
vars:
LEN: '{{ .LEN | default 72 }}'
cmds:
- >
{{.AUTHELIA_DOCKER}}
crypto hash generate pbkdf2 --variant sha512 --random --random.length {{.LEN}} --random.charset rfc3986
format-py-files:
cmds:
- >-
docker run --rm -u {{.USER_ID}}:{{.GROUP_ID}} -v $PWD:/app -w /app pyfound/black:latest_release black .
+82
View File
@@ -0,0 +1,82 @@
# Алерты на проблемные контейнеры
## Контекст
Случай с wakapi: при старте упали миграции, контейнер встал в restart-loop и
несколько дней крутился по кругу — никто не узнал. Из этого две проблемы:
1. Контейнеры могут бесконечно перезапускаться при ошибке.
2. Нет алертов о таких ситуациях.
## Что есть и что использовать
- **Netdata** — Docker-collector через cgroups + Docker API: состояние,
restart count, healthcheck status. Алерты в `health.d/*.conf`, нотификации
через `health_alarm_notify.conf` (Telegram/Discord/email/ntfy).
- **Dozzle** — только для просмотра логов после факта, нормальных алертов нет.
- **Caddy** — мог бы участвовать в healthcheck снаружи, но это отдельный слой.
## План — три слоя
### 1. Healthchecks в compose (фундамент)
Без них Docker считает контейнер «running», пока процесс жив, — wakapi с
падающими миграциями этому условию удовлетворял. Добавить в каждый
`docker-compose.yml.j2`:
```yaml
healthcheck:
test: ["CMD", "wget", "-q", "-O-", "http://localhost:PORT/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s # окно на миграции — failed-проверки до истечения не считаются
```
`start_period` — ключевая штука для случая wakapi: даём миграциям отработать,
до его истечения healthcheck не убивает контейнер.
### 2. Алерты через Netdata (главное)
Два разных сигнала:
- **Restart loop** — алерт на `docker.container_state` или счётчик
перезапусков (растёт > N за M минут). Это и есть «контейнер крутится по
кругу».
- **Unhealthy** — после healthcheck выше алерт на
`docker.container_health_status != healthy` дольше M минут.
Канал нотификаций: один, проще всего Telegram-бот. Настройка в
`health_alarm_notify.conf`.
### 3. Restart policy — что менять (или не менять)
Скорее **оставить `unless-stopped`**. Альтернативы и их минусы:
- `on-failure:5` — Docker сам остановит после 5 попыток. Минус: после ребута
сервера сервис не поднимется (только `always`/`unless-stopped` встают на
старте докера). Серьёзный регресс для домашнего сервера.
- Внешний sidecar, слушающий `docker events` и останавливающий контейнер
после N рестартов в окне — лишняя сложность ради того, что уже сделает
алерт.
Лучше: алерт пришёл → решаем вручную, остановить или чинить.
## Опционально (вне netdata)
- **Uptime Kuma** — внешний HTTP-чек по публичным URL. Ловит случаи, когда
контейнер «здоров», но прокся/DNS/Caddy сломаны. Свои нотификации, дашборд.
Не дублирует netdata, проверяет с другой стороны.
## Шаги при реализации
1. Добавить healthcheck + start_period в compose-шаблоны (начать с wakapi,
потом по списку).
2. Проверить, что netdata собирает Docker-метрики (collector включён).
3. Настроить один канал нотификаций (Telegram/ntfy/email — выбрать).
4. Написать пару алертов: restart-loop и unhealthy.
## Открытые вопросы
- Какой канал нотификаций использовать.
- Добавлять ли Uptime Kuma сразу или потом.
+285
View File
@@ -0,0 +1,285 @@
# Gitea runner on-demand в Yandex Cloud
## Контекст
В YC планируется развернуть self-hosted раннер для Gitea Actions. Сборки —
несколько раз в неделю, в среднем ~10 в неделю по ~5 минут. ВМ 24/7 даёт
утилизацию в районе 1%, остальное оплачивается впустую.
Цель — раннер активен только во время сборки и небольшого окна простоя
после, без ручных команд от разработчика.
## Архитектура
```
push → Gitea ──webhook──► Cloud Function ──Compute API──► ВМ (раннер)
(HMAC validate, │
start logic) ▼
act_runner (docker)
probe + decide
└─REST self-stop
```
Без API Gateway: функция публикуется напрямую через свой HTTPS-эндпоинт
`https://functions.yandexcloud.net/<id>`. Этот URL вписывается в Gitea
webhook. Аутентификация — HMAC-SHA256 в заголовке `X-Gitea-Signature`,
проверяется внутри функции.
Поток событий:
1. Push в Gitea → System Webhook на URL функции.
2. Функция валидирует HMAC, читает state ВМ, действует по стейт-машине
(см. ниже).
3. ВМ стартует, docker поднимает контейнер `act_runner`, тот подключается
к Gitea и забирает джобу.
4. На ВМ работают probe (раз в 30 сек собирает телеметрию) и decide
(раз в 1 мин принимает решение).
5. После idle-окна decide дёргает Compute REST API на gas самой себя.
## Cloud-side
### Ресурсы в YC
- Один фолдер на старте — общий с Gitea-сервером. Принятый риск: SA
самогашения формально может остановить любую ВМ в фолдере. Перенос в
отдельный фолдер — миграция на потом.
- Два сервисных аккаунта:
- `runner-self-stop` (привязан к ВМ): `compute.instances.stop`,
`compute.instances.get`.
- `runner-starter-fn` (привязан к функции): `compute.instances.start`,
`compute.instances.get`.
- Cloud Function `runner-starter`, runtime Python 3.11, 256 MB, timeout
30 сек. Публичный HTTPS-эндпоинт включён.
- Алерт Cloud Monitoring: `compute.instance.status = RUNNING` дольше 24 ч
подряд → нотификация (канал — на этапе внедрения).
### Bootstrap-скрипты
```
scripts/
├── runner-starter/ # код Cloud Function
│ ├── handler.py # webhook → start, стейт-машина
│ └── requirements.txt
├── runner_bootstrap.py # one-time: создать SA, ВМ, функцию, алерт
└── runner_deploy_function.py # обновить версию функции (yc CLI)
```
Скрипты на Python поверх `yc` CLI (через `subprocess`). Идемпотентность —
проверкой существования ресурсов перед созданием. Terraform не вводим:
ресурсов мало, оверкилл.
### Стейт-машина функции
| State ВМ | Действие |
| ------------------------------------- | ------------------------------------------------------- |
| `RUNNING`, `STARTING`, `RESTARTING` | 200, ничего не делаем |
| `STOPPED` | `instances:start` → 200 |
| `STOPPING` | poll до `STOPPED` (до 25 сек), затем `start` → 200 |
| `PROVISIONING`, `UPDATING` | 503 (временное состояние, retry клиентом) |
| `ERROR`, `CRASHED` | 500 + лог ошибки (нужен человек) |
| `DELETING`, `DELETED` | 500 + лог ошибки (что-то очень не так) |
## Host-side
### ВМ
- 2 vCPU (100%), 4 GB RAM, 25 GB network-hdd.
- Ubuntu 22.04 LTS.
- Без публичного IP при возможности (все исходящие к Gitea — через NAT
или внутренний адрес).
- Привязан SA `runner-self-stop`.
- Регистрация в Gitea Actions делается **один раз** при первой настройке.
Registration token берётся в Site Admin → Actions → Runners, кладётся
в Vault. Плейбук проверяет наличие `.runner` файла на ВМ; если есть —
пропускает регистрацию.
### Плейбук `playbook-gitea-runner.yml`
Стандартная структура проекта:
- `roles/owner` — пользователь `gitea-runner` (uid/gid выделить
отдельные, в группе `docker`).
- `files/gitea-runner/`:
- `docker-compose.template.yml``act_runner` в docker
(`gitea/act_runner:<pinned>`), `restart: unless-stopped`, mount
docker socket для запуска job-контейнеров.
- `act-runner-config.template.yaml` — конфиг раннера.
- `runner-probe.template.py` + `runner-probe.template.service` +
`runner-probe.template.timer`.
- `runner-decide.template.py` + `runner-decide.template.service` +
`runner-decide.template.timer`.
- `samples-logrotate.template.conf` — ротация `samples.log`.
Расширения шаблонов — `.template.<ext>`, не `.j2` (соглашение проекта).
### Раннер в docker
`act_runner` стартует через `docker compose up -d` под пользователем
`gitea-runner`. Поскольку `restart: unless-stopped`, дополнительный
systemd-юнит для самого раннера не нужен — после `Start` ВМ docker
поднимет контейнер автоматически.
Идентификатор контейнера фиксированный (`gitea_runner_app`), чтобы probe
мог исключать его из счёта.
### Probe и decide
Два независимых юнита, телеметрия — append-only лог.
`runner-probe` (timer раз в 30 сек):
```bash
# pseudocode
busy_count=$(docker ps -q | grep -v <runner_container_id> | wc -l)
state=$([ "$busy_count" -gt 0 ] && echo busy || echo idle)
echo "$(date -u +%FT%TZ) $state containers=$busy_count" \
>> /var/lib/runner-idle/samples.log
```
В реальной реализации — Python, фильтр по docker SDK или по результату
`docker ps --format '{{.Names}}'`.
`runner-decide` (timer раз в 1 мин):
1. Читает хвост `samples.log`.
2. Находит `last_busy_at` — timestamp последней `busy`-строки.
3. Находит `last_sample_at` — timestamp последней любой строки.
4. Логика:
- `now - last_sample_at > STALE_THRESHOLD` (5 мин) → **probe сломан**,
не гасим, логируем error. Алерт CM поймает по uptime.
- `now - last_busy_at > IDLE_THRESHOLD` (10 мин) → `instances:stop`
через REST.
- Иначе → ничего.
Параметры (`IDLE_THRESHOLD`, `STALE_THRESHOLD`) — переменные в шаблоне,
тюнятся по эксплуатации.
### Самогашение через REST
Без `yc` CLI. Decide-скрипт получает IAM-токен из metadata-сервиса и
дёргает Compute REST:
```python
TOKEN_URL = "http://169.254.169.254/computeMetadata/v1/instance/" \
"service-accounts/default/token"
ID_URL = "http://169.254.169.254/computeMetadata/v1/instance/id"
HEADERS = {"Metadata-Flavor": "Google"}
token = requests.get(TOKEN_URL, headers=HEADERS).json()["access_token"]
instance_id = requests.get(ID_URL, headers=HEADERS).text
requests.post(
f"https://compute.api.cloud.yandex.net/compute/v1/instances/{instance_id}:stop",
headers={"Authorization": f"Bearer {token}"},
)
```
Никаких файлов с SA-key, никаких зависимостей сверх `python3 +
requests`.
## Страховки от зависшей ВМ
Главный failure mode — probe или decide молча сломались, ВМ работает 24/7.
Слой 1 — soft idle-stop в decide. Нормальная работа.
Слой 2 — probe-staleness в decide. Если `samples.log` не обновляется
дольше `STALE_THRESHOLD` — логируем error, **не гасим** (мог идти
длинный билд). Полагаемся на слой 3.
Слой 3 — внешний алерт через Cloud Monitoring на uptime ВМ > 24 ч. Не
дёргает остановку, только нотификация. Порог высокий, чтобы дни активной
отладки не триггерили его. Если фактически висит сутки — это сигнал
смотреть руками.
Hard-cap по uptime внутри decide **не делаем**: ломает кейс «активно
тестирую несколько часов подряд», когда busy-сэмплы есть и логика идёт
правильно.
## Секреты (Vault, `vars/secrets.yml`)
| Ключ | Назначение |
| --------------------------------- | ----------------------------------------------------- |
| `gitea_runner_registration_token` | одноразовый токен для `act_runner register` |
| `gitea_webhook_secret` | общий с функцией HMAC-секрет для webhook |
| `yc_runner_folder_id` | в каком фолдере живёт ВМ |
| `yc_runner_instance_id` | ID ВМ (заполняется после bootstrap) |
| `yc_runner_function_url` | URL функции для webhook (заполняется после bootstrap) |
## invoke-таски
| Таск | Что делает |
| ----------------------------- | ----------------------------------------------------------------------------- |
| `inv runner-bootstrap` | one-time: создаёт SA, ВМ, функцию, алерт. Идемпотентен. |
| `inv runner-deploy-function` | заливает новую версию `runner-starter`. |
| `inv runner-pl` | up → `ansible-playbook playbook-gitea-runner.yml` → down. С `try/finally`. |
| `inv runner-up` / `down` | ручной старт/стоп ВМ для дебага. |
| `inv runner-status` | state ВМ + хвост `samples.log` (через ssh). |
| `inv runner-ssh` | ssh на ВМ, поднимает её при необходимости. |
`runner-pl` — основной таск, единственный «штатный» путь обновления
конфига ВМ. Если плейбук падает посередине, `finally` всё равно гасит ВМ
(idle-watch её и так бы погасил, но явное лучше).
## Стоимость
Базовая ставка YC (USD, после повышения 1 мая 2026): vCPU 100% =
$0.010164/ч, RAM = $0.002705/ГБ·ч, network-hdd = $0.0000356/ГБ·ч.
Профиль: 10,75 ч активной ВМ в месяц.
| Конфиг (2 vCPU 100%, 4 GB RAM, 25 GB HDD) | $/мес |
| ----------------------------------------- | ----- |
| Compute (vCPU + RAM) при 10,75 ч | ~0.33 |
| Disk (HDD, 24/7) | ~0.64 |
| Cloud Function, Monitoring | 0.00 |
| **Итого** | **~1.0** |
Сравнение: эта же ВМ в режиме 24/7 ≈ $23/мес. Экономия — порядка 95%.
Дальнейшая оптимизация — диск (15 GB вместо 25, ещё ~$0.25/мес). Делать
не сейчас.
## Принятые риски
- **Общий фолдер с другими ВМ.** SA `runner-self-stop` теоретически
может погасить и Gitea-сервер, если тот переедет в YC рядом. Митигация
при появлении такой ВМ — перенос в отдельный фолдер.
- **Холодный старт ~60 сек.** Дизайн заявляет 40, реальность ближе к
60 (Ubuntu boot + docker pull + act_runner connect). Документируем как
«нормальная задержка первой джобы».
- **Регистрационный токен утерян.** При пересоздании диска ВМ нужен
новый токен из Gitea UI. Документируем процесс. Раз в годы — терпимо.
- **Probe сломан, ВМ висит.** Поймает алерт CM, ручное расследование.
## План внедрения
1. Создать в YC: 2 SA, ВМ, дисковый ресурс. Через `inv
runner-bootstrap` или вручную через консоль (выбираем по желанию на
этапе реализации).
2. Прогнать `inv runner-pl` на свежесозданной ВМ. С временно
уменьшенным `IDLE_THRESHOLD` (2 мин вместо 10) — чтобы тестировать
гашение быстро.
3. Зарегистрировать раннер в Gitea руками: получить registration token,
положить в Vault, повторить `inv runner-pl`.
4. Проверить, что раннер появился в Gitea UI и забирает тестовую джобу.
5. Проверить idle-watch: дать ВМ постоять, убедиться, что гасится.
6. Создать функцию `runner-starter` через `inv runner-deploy-function`.
Проверить ручным `yc serverless function invoke`.
7. Прописать System Webhook в Gitea на URL функции, секрет совпадает с
Vault.
8. Тестовый push → end-to-end проверка.
9. Поднять `IDLE_THRESHOLD` обратно до 10 мин.
10. Настроить алерт Cloud Monitoring на uptime > 24 ч.
11. Неделя наблюдения: лог функции, samples.log, uptime ВМ, счёт.
## Открытые вопросы
- **Канал нотификаций** для алерта Cloud Monitoring (Telegram, ntfy,
email) — выбрать на этапе настройки.
- **Тип executor** в act_runner — docker (по умолчанию) или host. Ходим
через docker, host-executor пока не обсуждается.
- **Webhook на pull request** — нужно или только push? По умолчанию
только push. Расширим, если возникнет PR-flow.
- **Перенос ВМ в отдельный фолдер** — когда в общем появится вторая
ВМ. Пока не критично.
+291
View File
@@ -0,0 +1,291 @@
# Журнал миграции в Timeweb
Хронология фактического переезда. План и архитектурные решения —
в [timeweb.md](timeweb.md). Здесь только то, что реально сделано,
с датами.
Новые записи — сверху.
---
## Шаг 7 — `run-app` тег + унификация registry (2026-05-22, выполнено)
По итогам аудита подготовительных задач выявлены и закрыты две
несостыковки:
### 7a. Пропущенный `run-app` тег в remembos
В `playbook-remembos.yml:73` была задача
`Restart docker compose services if config changed but not
docker-compose.yml` (условный рестарт через `state: restarted`,
триггер — изменение `config.toml` без изменения `docker-compose.yml`),
у неё не было тега `run-app`. На cutover'е при
`--skip-tags run-app` основной запуск пропустился бы (правильно), а
эта условная задача всё равно сработала бы (потому что её `when:`
истинно при первом деплое — конфиг создаётся), попыталась бы
рестартануть несуществующий compose-стек и упала. Тег добавлен.
### 7b. Унификация `registry_url` в docker_login
`playbook-homepage.yml` и `playbook-transcriber.yml` использовали
хардкод `registry_url: "cr.yandex"`, а `playbook-remembos.yml`
`'{{ yc_container_registry }}'` из vault. Привёл к одному виду:
теперь во всех трёх — `"{{ yc_container_registry }}"` из vault.
`docker_registry_prefix` в `vars/homepage.yml` и `vars/transcriber.yml`
не трогал — там полный image-prefix вида `cr.yandex/<org-id>`,
это отдельная концепция (есть отдельный vault-var
`yc_container_registry_repository`, используемый в
`files/remembos/docker-compose.template.yml`). Если позже захочется
унифицировать целиком — это отдельная итерация.
### Аудит бэкапов: gap'ы по `caddyproxy`, `remembos`, `transcriber`
Эти три приложения имеют состояние в `data_dir`, но не имеют ни
`backup.template.sh`, ни ansible-генерируемого `backup-targets`.
Для миграции это закрывается через **rsync** на cutover'е — данные
переносятся напрямую, без зависимости от restic-снапшотов:
- `caddyproxy/data/` — TLS-сертификаты Let's Encrypt (важно, чтобы
не упереться в rate-limit LE при перевыпуске ~17 сертов).
- `remembos/data/` — user data (memos-токен, telegram tokens).
- `transcriber/data/` — пользовательские транскрипции.
Это означает: на этапе rsync (шаг 4 cutover'а в плане) **нельзя**
полагаться только на restic-restore — для этих трёх апов rsync —
единственный канал. Для остальных приложений (которые имеют
`backup.sh` или `backup-targets`) можно при необходимости использовать
restic как фолбэк, но rsync всё равно остаётся основным методом.
Долгосрочно — добавить им backup-механизм отдельной итерацией после
миграции. Сейчас это сверх сферы.
---
## Шаг 6 — `vars/vars.yml` загружается во всех плейбуках (2026-05-22, выполнено)
Сегодняшний коммит `8378f0e` («Migration: expose some public vars»)
вынес общие переменные (`application_dir`, `host_name`, `primary_user`,
`primary_user_uid`, `primary_user_gid`, `bin_prefix`,
`apprise_external_port`, `apprise_external_url`, `caddy_logs_dir`) из
vault в `vars/vars.yml`. Но большая часть плейбуков загружала только
`vars/secrets.yml` — на текущем сервере они работали лишь потому, что
inventory дублирует `application_dir` как override. На чистом
Timeweb-инвентаре без override они бы упали с undefined.
Прошёлся по всем плейбукам, добавил `- vars/vars.yml` сразу после
`- vars/secrets.yml`:
```
playbook-authelia.yml playbook-netdata.yml
playbook-calibre.yml playbook-outline.yml
playbook-docker.yml playbook-remembos.yml
playbook-dozzle.yml playbook-rssbridge.yml
playbook-eget.yml playbook-transcriber.yml
playbook-gitea.yml playbook-transcriber-registry.yml
playbook-gramps.yml playbook-tuwunel.yml
playbook-homepage.yml playbook-ufw.yml
playbook-homepage-registry.yml playbook-upgrade.yml
playbook-memos.yml playbook-wakapi.yml
playbook-miniflux.yml playbook-wanderer.yml
```
(21 файл — все «обычные» плейбуки, которые ещё не подключали vars.yml.)
Aggregator'ы `playbook-all-applications.yml` и `playbook-all-setup.yml`
не трогал — у них нет собственных `vars_files`, они используют
`import_playbook`, каждый импортируемый плейбук уже сам подключает
`vars.yml`.
`yamllint` чист. Идемпотентность проверить отдельным прогоном.
Проверить прогоном `inv pl -- all-applications` (или хотя бы
`inv pl -- gitea outline miniflux`) на текущем сервере — diff
ожидается пустой.
---
## Шаг 5 — переезд default application_dir на /srv (2026-05-22, выполнено)
`/mnt` по FHS — место для точек монтирования внешних дисков; на
системном диске Timeweb (фаза 1) это семантически неверно. Поменяли
дефолт на `/srv/applications` (FHS: «data for services provided by
this system»), для текущего YC-сервера сделали override в инвентаре.
Изменения:
- `vars/vars.yml``application_dir: "/srv/applications"`
(комментарий обновлён).
- `production.yml`у хоста `server` добавлен override
`application_dir: "/mnt/applications"`.
- `playbook-system.yml` — добавлен `vars/vars.yml` в `vars_files`,
захардкоженный `/mnt/applications` в задачах
`Create directory for mount` и `Mount external storages` заменён
на `{{ application_dir }}`.
- `playbook-remove-user-and-app.yml` — то же самое (`vars/vars.yml`
в `vars_files` + `{{ (application_dir, user_name) | path_join }}`).
- `tasks.py` — новый helper `_application_dir()` читает значение
сначала из inventory (override), затем из `vars/vars.yml`. `login_as_app`
больше не содержит `/mnt/applications`.
Что остаётся хардкодом — только `/mnt/applications` в `production.yml`
как override, и это правильно.
На Timeweb-инвентаре (когда появится) можно либо не задавать
`application_dir` вовсе (применится дефолт `/srv/applications`), либо
задать явно — для читаемости.
Проверить прогоном `inv pl -- system` на текущем сервере (Yandex
Cloud) — ничего не должно поменяться, потому что inventory override
возвращает `/mnt/applications` и mount всё ещё включён. Diff ожидается
пустой.
### Восстановление restic-снапшотов после смены путей
Старые снапшоты записаны с путями `/mnt/applications/<app>`. На
Timeweb данные должны лежать в `/srv/applications/<app>`. У restic
нет встроенного «remap path» при restore, поэтому делается в два
шага: восстановить во временный каталог, затем `rsync` на новое
место с сохранением uid/gid (приложения уже созданы playbook'ом с
теми же uid/gid, см. шаг про подготовку target).
Пример — восстановить gitea на Timeweb-машине:
```bash
sudo /usr/local/sbin/restic-shell.sh
# Распакуем нужную поддиректорию во временный каталог
restic restore latest \
--target /tmp/restic-restore \
--include /mnt/applications/gitea
# Перенесём данные на новый путь, сохранив владельца/группу/ACL/xattr
sudo rsync -aAX --info=progress2 \
/tmp/restic-restore/mnt/applications/gitea/ \
/srv/applications/gitea/
sudo rm -rf /tmp/restic-restore
```
Несколько приложений за один проход:
```bash
restic restore latest \
--target /tmp/restic-restore \
--include /mnt/applications/gitea \
--include /mnt/applications/outline \
--include /mnt/applications/miniflux
for app in gitea outline miniflux; do
sudo rsync -aAX --info=progress2 \
"/tmp/restic-restore/mnt/applications/$app/" \
"/srv/applications/$app/"
done
sudo rm -rf /tmp/restic-restore
```
Альтернатива через `restic mount` (если не хочется промежуточной
копии — данные мапятся как FUSE-FS):
```bash
sudo mkdir -p /mnt/restic-snapshots
restic mount /mnt/restic-snapshots &
sudo rsync -aAX \
/mnt/restic-snapshots/snapshots/latest/mnt/applications/gitea/ \
/srv/applications/gitea/
sudo fusermount -u /mnt/restic-snapshots
```
После переезда новые снапшоты будут записываться уже с путями
`/srv/applications/<app>` — никаких трюков для текущих бэкапов не
нужно.
---
## Шаг 4 — условное монтирование внешнего диска (2026-05-22, выполнено)
Задача `Mount external storages` в `playbook-system.yml` теперь
выполняется только при включённом флаге `mount_external_storage`
(default `false`). Сам UUID диска оставлен захардкоженным в
плейбуке — параметризовать не стали, потому что для Timeweb (фаза 1)
монтирование вообще не нужно, а для фазы 2 пока неизвестно, какой
UUID получится у второго диска.
Изменения:
- `playbook-system.yml` — у задачи mount добавлен
`when: mount_external_storage | default(false) | bool`.
- `production.yml` (инвентарь YC) — у хоста `server` добавлен
`mount_external_storage: true`, чтобы текущее поведение
сохранилось.
В будущем `timeweb.yml` просто не будет задавать эту переменную —
mount пропустится, `/mnt/applications` останется обычной директорией
на системном диске.
На фазе 2 (подключение медленного диска в Timeweb) UUID в
`playbook-system.yml` придётся поменять и включить флаг — это
осознанный шаг, не автоматизировано.
Проверено прогоном `inv pl -- system` на текущем сервере (Yandex
Cloud) — задача mount по-прежнему выполняется, `/mnt/applications`
смонтирован, изменений нет.
---
## Шаг 3 — переключение auth на cr.yandex (2026-05-22, выполнено)
Заменена аутентификация в Yandex Container Registry с YC-metadata
service на OAuth-token из vault.
Изменения:
- `files/yandex-docker-registry-auth.sh`**удалён**.
- `playbook-homepage.yml` — задача `ansible.builtin.script:
yandex-docker-registry-auth.sh` заменена на
`community.docker.docker_login` с `username: oauth`, `password:
"{{ yc_oauth_token }}"`.
- `playbook-transcriber.yml` — то же самое.
Локальные push-плейбуки (`playbook-homepage-registry.yml`,
`playbook-transcriber-registry.yml`) не трогал — там нет auth-задачи
в принципе, локальный docker аутентифицируется вручную
(`yc container registry configure-docker` или `docker login`).
Если позже захочется унифицировать — можно добавить тот же
`docker_login` с `delegate_to: 127.0.0.1`.
Проверено прогоном `inv pl -- homepage` и `inv pl -- transcriber` на
текущем сервере (Yandex Cloud) — ошибок нет, контейнеры работают.
Значит и на Timeweb заработает (единственная разница — исходящий IP,
а OAuth-токен в YC принимается извне).
---
## Шаг 2 — OAuth-token для cr.yandex (2026-05-22, выполнено)
В `vars/secrets.yml` добавлена (или обновлена) переменная
`yc_oauth_token` со свежим OAuth-токеном Яндекса. Токен будет
использоваться для логина в `cr.yandex` с новой машины Timeweb
(вместо текущего скрипта `files/yandex-docker-registry-auth.sh`,
который завязан на YC metadata service `169.254.169.254` и
работает только внутри YC).
Сам код переключения на `community.docker.docker_login` пока не
вносится — это следующая итерация. Сейчас токен просто положен в
vault, чтобы не делать этого в день cutover'а под прессом.
---
## Шаг 1 — снижение TTL DNS (2026-05-22, выполнено)
В админке Yandex 360 для зоны `vakhrushev.me` уменьшен TTL
A-записей с **21 600 с (6 ч)** до **1 200 с (20 мин)**. Это даёт
запас по времени на распространение изменений после смены IP в
день cutover'а — старые кэширующие резолверы перестанут отдавать
старый адрес максимум через 20 минут (вместо 6 часов).
Делается **заранее**, потому что само снижение TTL тоже
распространяется по кэшам по правилам старого TTL — то есть после
правки нужно подождать ≥ 6 часов, чтобы новое значение TTL само
успело прижиться. Раньше cutover'а нужно сделать с большим
запасом — день в день не сработает.
+553
View File
@@ -0,0 +1,553 @@
# Миграция сервера в Timeweb
## Контекст и цели
Сервер `rivendell-v2` переезжает с виртуальной машины в Yandex Cloud
(`158.160.46.255`) на VPS в Timeweb.
### Причины переезда
1. **Высокая стоимость.** Тариф в Yandex Cloud обходится в ≈ 2 900 ₽/мес
за конфигурацию, которая в Timeweb стоит ≈ 2 000 ₽/мес и при этом
мощнее по всем параметрам (см. сравнение ниже).
2. **Упор в потолок RAM.** Текущий сервер уже использует ≈ 80 %
доступной памяти на штатной нагрузке (см.
`project_server_specs`). Любой всплеск (миграции БД, индексация
в Outline, бэкап с restic) — и приложения начинают конкурировать
за память, появляются OOM-риски. Дальше расти на этом тарифе
некуда без значительного увеличения цены.
3. **Медленные диски.** Из-за высокой стоимости в YC приходится
использовать дешёвый HDD-том вместо SSD/NVMe — это заметно
снижает отзывчивость приложений (особенно Gitea, Outline,
тёплый старт контейнеров, рестики check/forget). На Timeweb за
меньшие деньги получаем NVMe.
Переезд решает все три проблемы одновременно: дешевле, больше
RAM, быстрее диск.
### Сравнение тарифов
| Параметр | Yandex Cloud | Timeweb Cloud VPS |
| -------------- | ----------------------------------------- | ------------------ |
| CPU | Intel Cascade Lake, vCPU 2, гарантия 50 % | 4 × 3.3 ГГц |
| RAM | 4 ГБ | 8 ГБ |
| Диск | 120 ГБ HDD | 80 ГБ NVMe |
| Публичный IP | да | да |
| **Цена/месяц** | **2 887 ₽** | **1 980 ₽** |
Итого: **907 ₽/мес (≈ −31 %)**, при этом **×2 RAM** (закрывает
причину 2), **×2 ядер**, гарантия CPU 100 % вместо 50 %,
**NVMe вместо HDD** (закрывает причину 3). Минус — диск меньше
(80 ГБ против 120 ГБ HDD), что и стало основанием для фазы 2 с
подключением второго «холодного» диска под крупные данные.
Переезжает **только compute** (VM с приложениями). Остальные сервисы
Yandex Cloud остаются на месте и продолжают использоваться с новой
машины:
- **Container Registry** — `cr.yandex/crplfk0168i4o8kd7ade` для образов
`homepage-nginx` и `transcriber`.
- **Object Storage (S3)** — restic-репозиторий `yandex_cloud_s3`.
- **Postbox SMTP** — `postbox.cloud.yandex.net` (gitea, gramps, wakapi,
outline, authelia, apprise).
- **Yandex 360 / DNS-зона** `vakhrushev.me` — там же управляются записи
и почтовый домен.
Параметры даунтайма — мягкие, это личная машина. Стратегия — «cold
cutover»: остановить сервисы на источнике, раскатать ansible на
target без запуска приложений, перенести данные с сохранением
uid/gid, запустить сервисы на target, переключить DNS.
Конфигурация target — Cloud VPS Timeweb с одним диском **80 ГБ** на
первой фазе. Позднее (отдельной фазой) будет подключён второй
«медленный» диск под крупные данные (`calibre`, бэкапы, возможно
`outline`).
---
> Фактическое выполнение переезда — в отдельном файле
> [timeweb-migration-log.md](timeweb-migration-log.md). Здесь только
> план и архитектурные решения.
---
## Инвентаризация YC-зависимостей в коде
| Компонент | Где | Что делать при переезде |
| --- | --- | --- |
| `production.yml` | `ansible_host: 158.160.46.255`, `ansible_user: major` | Заменить на новый IP/пользователя Timeweb |
| `files/yandex-docker-registry-auth.sh` | Логин в `cr.yandex` через **YC metadata service** (`169.254.169.254`) | **Не работает вне YC.** Перейти на static OAuth-token / IAM-token (новый скрипт + секрет в vault) |
| `playbook-system.yml` (mount-storage) | UUID `3942bffd-…` монтируется в `/mnt/applications` | Фаза 1: отключить mount или сделать UUID переменной vault. Фаза 2 (после подключения медленного диска): включить заново с новым UUID |
| `files/backups/config.template.toml` | `[storage.yandex_cloud_s3]` + `AWS_*` ключи | **Не меняем.** Тот же бакет/ключи продолжают работать. Меняется только `host_name` (для подписи снапшотов и нотификаций) — он уже шаблонится |
| SMTP (`postbox_host/port/user/pass`) | gitea, gramps, wakapi, outline, authelia, apprise | **Не меняем.** Postbox SMTP доступен извне YC по тем же credentials |
| `files/backups/rclone.template.conf` (`pr86keedav`) | WebDAV-копия restic — внешний сервис | **Не меняем** |
| Caddy `tls anwinged@ya.ru` | ACME | Не меняется, ACME перевыпустит сертификаты после смены IP |
Никаких других hardcoded YC-эндпоинтов в плейбуках / шаблонах нет —
SSH, ufw, fail2ban, docker, eget, restic, Caddy полностью переносимы.
---
## UID / GID — критично для rsync
UID/GID каждого приложения зафиксированы в плейбуках и в
`vars/homepage.yml` / `vars/transcriber.yml`. Роль `owner` создаёт
группы и пользователей **с явно указанными gid/uid**
(`roles/owner/tasks/main.yml`). Это значит:
- Если на новой машине **сначала** раскатать все плейбуки (без
запуска приложений), пользователи получатся с теми же uid/gid.
- Тогда `rsync -aAX` (с сохранением owner) корректно ляжет на target.
- Дополнительный maping uid не нужен.
Список приложений с uid/gid (для сверки и для документации):
```
caddyproxy 1010 / 1011
authelia 1011 / 1012
netdata 1012 / 1013
miniflux 1013 / 1014
rssbridge 1014 / 1015
wakapi 1015 / 1016
dozzle 1016 / 1017
transcriber 1017 / 1018
wanderer 1018 / 1019
memos 1019 / 1020
gitea 1005 / 1006
outline 1007 / 1008
homepage 1008 / 1009
gramps 1009 / 1010
calibre 1102 / 1102
remembos 1103 / 1103
apprise 1104 / 1104
tuwunel 1105 / 1105
goaccess 1106 / 1106
```
(Возможные пересечения uid одного приложения и gid другого
существуют, но Linux держит их в разных пространствах имён — не
страшно.)
---
## Подготовка кода проекта
Делается **до** аренды Timeweb-машины, отдельным PR (или сериями
коммитов на отдельной ветке). Цель — чтобы тот же ansible
работал и на источнике, и на target без условных хаков.
### 1. Заменить YC-specific docker registry auth
`files/yandex-docker-registry-auth.sh` сейчас использует metadata
service (`169.254.169.254`). Это работает только внутри YC VM,
поэтому на Timeweb его надо заменить.
**Решение — OAuth-token Яндекса.** Простой и достаточный для
домашнего сервера механизм:
1. Получить OAuth-token в кабинете Яндекса:
<https://oauth.yandex.ru/authorize?response_type=token&client_id=1a6990aa636648e9b2ef855fa7bec2fb>
(стандартный client_id для `yc` CLI, токен с правом доступа к
Container Registry).
2. Положить в `vars/secrets.yml` как `yc_oauth_token` (vault).
3. Переписать `files/yandex-docker-registry-auth.sh` как шаблон
(`.template.sh`) и рендерить через `ansible.builtin.template`
вместо `script:`. Скрипт сводится к:
```sh
#!/usr/bin/env sh
set -eu
echo "{{ yc_oauth_token }}" | \
docker login --username oauth --password-stdin cr.yandex
```
Альтернатива — не рендерить, а передавать токен в скрипт
аргументом или через переменную окружения, чтобы не светить его
в системе.
4. В `playbook-homepage.yml` и `playbook-transcriber.yml` поменять
`ansible.builtin.script:` на `ansible.builtin.template:` +
`ansible.builtin.command:` (либо использовать модуль
`community.docker.docker_login` напрямую с `username: oauth`,
`password: "{{ yc_oauth_token }}"` — это самый чистый вариант,
тогда отдельный скрипт вообще не нужен).
5. То же самое — для локальных push-плейбуков
`playbook-homepage-registry.yml` и
`playbook-transcriber-registry.yml`.
Рекомендую вариант с `community.docker.docker_login` — это убирает
shell-скрипт целиком и сильно проще.
Минусы OAuth-token: токен живёт долго и даёт доступ ко всему
аккаунту Яндекса. Для личного сервера приемлемо; если позже
захочется минимизировать blast radius — заменить на IAM-key
сервисного аккаунта (отдельная итерация после миграции).
Затронутые места: `files/yandex-docker-registry-auth.sh` (удалить
или переписать), `playbook-homepage.yml`, `playbook-transcriber.yml`,
`playbook-homepage-registry.yml`, `playbook-transcriber-registry.yml`,
`vars/secrets.yml` (новый ключ `yc_oauth_token`).
### 2. Сделать опциональным монтирование внешнего диска
Сейчас `playbook-system.yml` жёстко монтирует UUID `3942bffd-…` в
`/mnt/applications`. На Timeweb этого диска нет.
Минимальная правка — вытащить UUID в переменную (`storage_uuid`) и
обернуть mount-задачу `when: storage_uuid is defined`. В
`vars/secrets.yml` или `vars/vars.yml` для текущего сервера задать
UUID, для Timeweb (фаза 1) — не задавать. На фазе 2 (когда придёт
медленный диск) — задать новый UUID.
Альтернатива: вынести параметры в инвентарь
(`production.yml` → `host_vars/server.yml`).
При этом сама директория `/mnt/applications` должна создаваться в
любом случае — playbook уже это делает, надо лишь убедиться, что
задача «Create directory for mount» не зависит от mount-задачи.
### 3. Параметризовать инвентарь
На время перехода — **два отдельных файла**: текущий
`production.yml` остаётся как есть, рядом появляется новый
`timeweb.yml` с настройками Timeweb-машины. Все ansible-команды
во время миграции явно указывают `-i timeweb.yml`. После того, как
переезд закончен и старая машина выключена — `production.yml`
просто удаляется, `timeweb.yml` переименовывается в
`production.yml`.
`tasks.py` использует `yq` для извлечения `ansible_host` / `ansible_user`
из инвентаря (`_yq(".ungrouped.hosts.server…")`) — путь к файлу
зашит константой `HOSTS_FILE = "production.yml"`. Варианты:
- На время миграции временно поменять `HOSTS_FILE = "timeweb.yml"`
в локальном коммите (или через env override), потом откатить — после
переименования всё снова работает.
- Принять, что `inv ssh / zj / btop / login` работают только с
активным сервером (тем, что в `production.yml`), а к старой
машине во время миграции ходим напрямую через `ssh
major@158.160.46.255`.
Первый вариант чище. Достаточно одной строчки правки.
### 4. Прочее
- `README.md` — обновить инструкцию по DNS и упомянуть Timeweb.
- Удалить (или пометить deprecated) yandex-метаданные в комментариях
`yandex-docker-registry-auth.sh`.
- Проверить, что у всех application-плейбуков задача с
`community.docker.docker_compose_v2: state: present` помечена
тегом `run-app` — это позволит раскатывать `--skip-tags run-app`
для подготовки target без запуска контейнеров. Сейчас тег `run-app`
есть в большинстве плейбуков, но надо пройтись и убедиться, что
он покрывает **все** контейнеры (включая calibre, dozzle,
remembos, transcriber, tuwunel, wanderer, memos).
---
## Подготовка target-машины
1. Заказать Cloud VPS в Timeweb:
- Ubuntu LTS (та же мажорная версия, что и сейчас — упростит
совместимость пакетов).
- 4 GB RAM (текущий лимит ≈ 3.8 GiB, см. `project_server_specs`),
можно взять чуть с запасом — 4–6 GB, иначе netdata + tuwunel +
outline начнут давить.
- 2 vCPU.
- SSD 80 ГБ.
- Снять/настроить firewall провайдера (или отключить, т.к. у нас
свой ufw).
2. Создать пользователя с правами sudo (аналог `major`), залить
свой SSH-ключ.
3. Добавить хост в инвентарь как `server` (или временный
`timeweb`), убедиться, что `ansible -m ping` отвечает.
4. Снизить TTL DNS-записей в Yandex 360 до 60300 секунд **за
~2448 часов** до cutover.
---
## Cutover (план дня X)
Предусловия: код выкатан, target-машина пингуется по ansible, TTL
DNS снижены.
### Шаг 1. Финальный бэкап на источнике
```bash
inv ssh
sudo /usr/local/sbin/backup-all.py 2>&1 | tee /tmp/final-backup.log
```
Убедиться, что в логе все приложения отработали успешно и в S3
появился свежий restic-snapshot (на случай отката или потери
данных при rsync).
### Шаг 2. Остановить все приложения на источнике
Останавливаем docker-демон целиком — это атомарно гасит все
контейнеры за один вызов, не зависит от текущего списка приложений
и шлёт корректный SIGTERM (с грейс-периодом ~15 сек) каждому, что
функционально эквивалентно `docker compose down` по всем стекам.
```bash
inv ssh
sudo systemctl stop docker.service docker.socket
sudo systemctl disable docker.service docker.socket # страховка от автостарта при ребуте
sudo systemctl stop cron # чтобы ночной backup-cron не побежал
```
Финальный бэкап (шаг 1) **обязательно** должен пройти до этого
момента — `backup-all.py` запускает скрипты приложений, которые
делают `docker compose exec ... pg_dump ...`; без работающего
daemon это сломается.
`disable` — страховка: если по какой-то причине старая машина
перезагрузится во время rsync (или мы вернёмся на источник для
проверки/отката), docker не поднимется автоматически и сервисы
не начнут писать в данные, которые мы уже считаем «фиксированной
копией». В случае отката — `enable` + `start` обратно.
Проверить, что `docker ps` сейчас отвечает «daemon not running»
(или вернёт пустой список — зависит от того, как `inv ssh` пройдёт
до/после стопа). Если нужно убедиться, что контейнеры реально
ушли — `ps auxf | grep -E "containerd|docker" | grep -v grep`.
### Шаг 3. Раскатать инфраструктуру на target БЕЗ запуска приложений
```bash
# 1) системная база
uv run ansible-playbook -i timeweb.yml --diff playbook-all-setup.yml
# 2) приложения (создаём пользователей, каталоги, конфиги,
# но НЕ запускаем контейнеры)
uv run ansible-playbook -i timeweb.yml --diff \
--skip-tags run-app \
playbook-all-applications.yml
```
Цель — после этого на target есть:
- Корректные uid/gid для всех приложений.
- Каталоги `/srv/applications/<app>/{data,config,backups}` (на
Timeweb дефолт изменён с `/mnt/applications`; см.
[журнал шаг 5](timeweb-migration-log.md)).
- Шаблоны `docker-compose.yml` и application-конфиги — отрендерены
и лежат на месте.
- Docker и сети созданы.
- ufw настроен, fail2ban работает.
### Шаг 4. Перенос данных
Пути меняются: на YC данные лежат в `/mnt/applications/<app>`, на
Timeweb — в `/srv/applications/<app>`. Rsync делает remap сам
(потому что мы указываем источник и приёмник явно). Для трёх
приложений без backup-механизма (`caddyproxy`, `remembos`,
`transcriber`) rsync — **единственный** канал переноса, restic
для них не альтернатива.
**Вариант A — rsync напрямую (основной путь).** С target-машины
тянем данные со старой:
```bash
sudo rsync -aAX --info=progress2 --delete \
--exclude='lost+found' \
major@158.160.46.255:/mnt/applications/ \
/srv/applications/
```
`-aAX` сохраняет ACL/xattrs и uid/gid (численные значения).
Численные uid/gid на target совпадают с источником, потому что
плейбуки на обеих машинах создают пользователей с одинаковыми
явно заданными `app_owner_uid`/`gid`.
Каждое приложение можно тянуть отдельно — удобнее наблюдать
прогресс и можно частично пересинхронизировать в случае ошибок:
```bash
sudo rsync -aAX --info=progress2 --delete \
major@158.160.46.255:/mnt/applications/gitea/ \
/srv/applications/gitea/
```
**Вариант B — restore из restic (страховка).** Если по сети
источник недоступен или хочется проверить, что бэкапы вообще
рабочие. Подробный пример (с учётом смены `/mnt` → `/srv`) — в
[журнале миграции, шаг 5](timeweb-migration-log.md).
Для `caddyproxy`, `remembos`, `transcriber` использовать B
**нельзя** — у них нет архивации, в restic-снапшоте данных просто
нет. Только A.
Рекомендую **A как основной метод**, B держим как страховку
для приложений, у которых есть восстановимый снапшот.
### Шаг 5. Запуск приложений на target
Раскатываем application-плейбуки ещё раз — теперь без `--skip-tags`:
```bash
uv run ansible-playbook -i timeweb.yml --diff \
playbook-all-applications.yml
```
Этот же запуск проверит идемпотентность шаблонов (не должно быть
diff'ов кроме docker-up).
После старта — проверить:
- `docker ps` — все контейнеры в healthy.
- Локально (по IP) `curl http://<target-ip>` — Caddy отвечает (на
редирект, т.к. сертификаты ещё не выпущены под этим IP).
- Логи Caddy — выпуск сертификатов запустится после смены DNS, не
раньше. Это нормально.
### Шаг 6. Переключение DNS
В Yandex 360 admin (`admin.yandex.ru/domains/vakhrushev.me`)
поменять A-записи для всех subdomain'ов на новый IP. Перечень
поддоменов (из `Caddyfile.template`):
```
vakhrushev.me (apex)
matrix.vakhrushev.me
auth.vakhrushev.me
status.vakhrushev.me
git.vakhrushev.me
outline.vakhrushev.me
gramps.vakhrushev.me
miniflux.vakhrushev.me
wakapi.vakhrushev.me
wanderer.vakhrushev.me
memos.vakhrushev.me
remembos.vakhrushev.me
calibre.vakhrushev.me
wanderbase.vakhrushev.me
rssbridge.vakhrushev.me
dozzle.vakhrushev.me
goaccess.vakhrushev.me
```
После смены — подождать пока TTL разойдётся, проверить через
`dig +short <hostname>` с независимой машины.
Caddy сам пойдёт за сертификатами Let's Encrypt — следить за его
логами (`docker logs caddyproxy_app -f`).
### Шаг 7. Проверка после cutover
Чеклист (примерно по приоритету):
- [ ] `vakhrushev.me` отвечает 200, отдаёт homepage.
- [ ] `auth.vakhrushev.me` — Authelia, можно залогиниться.
- [ ] `git.vakhrushev.me` — Gitea, репозитории на месте, ssh-доступ
(порт 2222 в ufw уже открыт).
- [ ] `outline.vakhrushev.me` — открывается, документы на месте.
- [ ] `matrix.vakhrushev.me` — Tuwunel/Element подключается;
federation проверяется через
<https://federationtester.matrix.org/>.
- [ ] `miniflux.vakhrushev.me`, `wakapi.vakhrushev.me`,
`memos.vakhrushev.me`, `gramps.vakhrushev.me`,
`remembos.vakhrushev.me`, `wanderer.vakhrushev.me`,
`calibre.vakhrushev.me`, `rssbridge.vakhrushev.me`,
`dozzle.vakhrushev.me`, `goaccess.vakhrushev.me` —
открываются, данные на месте.
- [ ] Netdata `status.vakhrushev.me` — собирает метрики.
- [ ] Backup-cron — следующий запуск (1:00) проходит успешно,
приходит уведомление в apprise.
- [ ] SMTP — отправить тестовое письмо из gitea/authelia (триггер
reset password).
- [ ] Container Registry — `docker pull cr.yandex/...` на новой
машине проходит (это значит, что наша новая аутентификация
через OAuth/IAM работает).
### Шаг 8. Заморозка источника
Когда всё подтверждено стабильным (≥ 24 часа):
- Остановить и выключить старую VM в YC.
- Подождать неделю-две на случай отката.
- Удалить VM и связанные ресурсы (только compute! S3-бакет с
restic-бэкапами и Container Registry **остаются**).
- Удалить `production.yml`, переименовать `timeweb.yml` →
`production.yml`, откатить временную правку `HOSTS_FILE` в
`tasks.py` (теперь снова `production.yml`). Закоммитить.
---
## Фаза 2: подключение медленного диска
После того как Timeweb-сервер стабилен:
1. Заказать дополнительный «холодный» диск в Timeweb, прицепить
к VPS.
2. Узнать UUID нового устройства (`lsblk -f`).
3. Решить, куда монтировать — варианты:
- Сохранить текущую схему (`/mnt/applications` на медленном
диске целиком). Минус: всё IO приложений уходит на медленный
диск.
- **Лучше:** оставить `/mnt/applications` на быстром SSD,
медленный смонтировать как `/mnt/cold` и под calibre/большие
бэкапы делать bind-mount или поменять `data_dir` у нужных
приложений.
4. Восстановить в `playbook-system.yml` mount-задачу с новым
UUID (через переменную, заведённую на фазе 1).
5. Прогнать `inv pl -- system` с тегом `mount-storage`.
6. Переехать на холодный диск только большие данные. Для calibre
это означает остановить контейнер, `rsync` библиотеки книг,
поправить `data_dir` в `vars`, запустить.
---
## Что НЕ менять во время миграции
Чтобы не накапливать изменения в одном переезде:
- Версии docker-образов всех приложений — те же, что в источнике.
- Конфиги приложений — без правок.
- Restic snapshot policy.
- Apprise/notification каналы.
Любые улучшения (healthchecks из `docs/drafts/alerts.md`,
gitea runner и т.п.) — отдельным циклом после миграции.
---
## Откат
Если на target что-то критично сломалось:
1. DNS возвращаем обратно на старый IP.
2. Старая VM в YC жива и заглушена → стартуем её, поднимаем
сервисы (`docker compose up -d` под каждым пользователем).
3. Изучаем, в чём дело на target, лечим, повторяем cutover.
Поэтому шаг «Заморозка источника» отделён от «удаления» — у нас
есть «горячее запасное» как минимум на пару дней.
---
## Открытые вопросы
На текущей итерации — нет, все ключевые развилки закрыты:
- ~~Auth для cr.yandex~~ → OAuth-token Яндекса (`yc_oauth_token` в
vault, `community.docker.docker_login` в плейбуках).
- ~~Инвентарь~~ → два отдельных файла, после cutover `timeweb.yml`
переименовывается в `production.yml`.
- ~~Регион/TZ Timeweb~~ → совпадает с текущим.
- ~~IP-whitelist в конфигах~~ → отсутствует, смена IP безопасна.
- ~~Объём данных vs 80 ГБ~~ → 22 ГБ всего, из них calibre 16 ГБ;
с запасом влезает в фазе 1, второй диск не на критическом пути.
Возможные вопросы по ходу реализации (выяснятся в процессе):
- Конкретная процедура получения OAuth-token Яндекса (через
`oauth.yandex.ru` или через `yc` CLI).
- Поведение Caddy при первом выпуске сертификатов после смены DNS —
убедиться, что rate-limit Let's Encrypt не упрётся (≈ 17
поддоменов выпускаются сразу, лимит LE — 50 сертификатов в неделю
на registered domain, запас есть).
- Federation Matrix после смены IP — обычно достаточно того, что
apex `vakhrushev.me` отдаёт `.well-known/matrix/server`, но
стоит проверить через `federationtester.matrix.org` сразу после
cutover.
+23
View File
@@ -0,0 +1,23 @@
services:
apprise_app:
image: caronc/apprise:v1.3.3
container_name: apprise_app
restart: unless-stopped
ports:
- "127.0.0.1:{{ apprise_external_port }}:8000"
networks:
web_proxy_network:
aliases:
- "apprise"
volumes:
- "{{ config_dir }}:/config"
environment:
PUID: "{{ owner_create_result.uid }}"
PGID: "{{ owner_create_result.group }}"
APPRISE_STATEFUL_MODE: simple
APPRISE_WORKER_COUNT: 1
networks:
web_proxy_network:
external: true
+2
View File
@@ -0,0 +1,2 @@
tgram://{{ notifications_tg_bot_token }}/{{ notifications_tg_chat_id }}
mailtos://{{ postbox_user }}:{{ postbox_pass }}@{{ postbox_host }}:{{ postbox_port }}/?from=notifications@vakhrushev.me&to={{ notifications_email }}
@@ -731,10 +731,18 @@ access_control:
subject: 'group:admins'
policy: 'two_factor'
- domain: 'goaccess.vakhrushev.me'
subject: 'group:admins'
policy: 'two_factor'
- domain: 'wanderbase.vakhrushev.me'
subject: 'group:admins'
policy: 'two_factor'
- domain: 'remembos.vakhrushev.me'
subject: 'group:admins'
policy: 'two_factor'
- domain: 'rssbridge.vakhrushev.me'
subject: 'group:admins'
policy: 'one_factor'
+2 -2
View File
@@ -2,8 +2,8 @@ services:
authelia_app:
container_name: 'authelia_app'
image: 'docker.io/authelia/authelia:4.39.14'
user: '{{ user_create_result.uid }}:{{ user_create_result.group }}'
image: 'docker.io/authelia/authelia:4.39.19'
user: '{{ owner_create_result.uid }}:{{ owner_create_result.group }}'
restart: 'unless-stopped'
networks:
- "web_proxy_network"
-25
View File
@@ -1,25 +0,0 @@
$ANSIBLE_VAULT;1.1;AES256
39343035656562656632323766356561386665373036383564616331333333613765353737663632
3531663835303562393063343231623464663232333532380a663838663938316566616532623065
66336463643862626538366462346231386333366464323131363836326436373563623164336632
6234353437383432380a396136653136616335343936343335633236373363353766666539396334
36613836663831333838633231363731323234323761306630646632616238363662376462333039
32373938343562313064663334383766653161613032623936646361316561666532356465623133
32303663313834663834366363383265653939316336356239313364623366386631626536643439
31333362353961353434333636343336323239363461663937313931616262316330376165393263
63366665396431323034383939633365316134356564656136393032393864393636616234316231
37616336396435626264643232343766616364306264376338313238356261653863336535363237
34653638316161636431653465343536323331656230633332333139386132653433626662343837
35396437633233363637376561303338386432643039626336376366373334613463663465613637
36643734626163623738336435383032353837366532316566613864306430653336616637383262
65646131643533323563393133373964633863636666633338616236386531323064396137376232
37653333666566386563383235356232663338643161313635643661326339333661393135643030
62356662623365376662646166316262353964383936373463393339623961376232653664306439
36336231393434356661316336653033346430386366663138323832613532303265343136373836
64666561616535623732326464643831363866326265343165356330646561653066393764336134
30326436663066633163393163306265383834306634663639336437303965373063323335333537
38643234623061376565636536323563623739313165343464316466363364613963636437363830
33306632313839373132636130326331363538323763326333316165363633336561373030373963
38313135343464303331343866646634393162393361333962356133376163393865373239323763
31303336613937303031343532333036653133363439643864663661373639646566643831313662
35613430333861376565
+260 -206
View File
@@ -4,12 +4,14 @@ Backup script for all applications
Automatically discovers and runs backup scripts for all users,
then creates restic backups and sends notifications.
"""
import itertools
import os
import sys
import subprocess
import logging
import pwd
import time
from abc import ABC
from dataclasses import dataclass
from pathlib import Path
@@ -42,16 +44,38 @@ logger = logging.getLogger(__name__)
@dataclass
class Config:
host_name: str
roots: List[Path]
@dataclass
class Application:
path: Path
owner: str
backup_script: Optional[Path]
backup_targets: List[Path]
@dataclass
class StorageRunResult:
name: str
success: bool
duration: float
def format_duration(seconds: float) -> str:
if seconds < 60:
return f"{seconds:.1f}s"
minutes = int(seconds // 60)
secs = int(seconds % 60)
if minutes < 60:
return f"{minutes}m{secs:02d}s"
hours = minutes // 60
minutes = minutes % 60
return f"{hours}h{minutes:02d}m{secs:02d}s"
class Storage(ABC):
name: str
def backup(self, backup_dirs: List[str]) -> bool:
"""Backup directories"""
raise NotImplementedError()
@@ -64,19 +88,15 @@ class ResticStorage(Storage):
self.name = name
self.restic_repository = str(params.get("restic_repository", ""))
self.restic_password = str(params.get("restic_password", ""))
self.aws_access_key_id = str(params.get("aws_access_key_id", ""))
self.aws_secret_access_key = str(params.get("aws_secret_access_key", ""))
self.aws_default_region = str(params.get("aws_default_region", ""))
if not all(
[
self.restic_repository,
self.restic_password,
self.aws_access_key_id,
self.aws_secret_access_key,
self.aws_default_region,
]
):
env_raw = params.get("env") or {}
if not isinstance(env_raw, dict):
raise ValueError(
f"'env' must be a table for storage backend ResticStorage: '{self.name}'"
)
self.env: Dict[str, str] = {str(k): str(v) for k, v in env_raw.items()}
if not self.restic_repository or not self.restic_password:
raise ValueError(
f"Missing storage configuration values for backend ResticStorage: '{self.name}'"
)
@@ -92,19 +112,13 @@ class ResticStorage(Storage):
return False
def __backup_internal(self, backup_dirs: List[str]) -> bool:
logger.info("Starting restic backup")
logger.info("Starting restic backup for storage '%s'", self.name)
logger.info("Destination: %s", self.restic_repository)
env = os.environ.copy()
env.update(
{
"RESTIC_REPOSITORY": self.restic_repository,
"RESTIC_PASSWORD": self.restic_password,
"AWS_ACCESS_KEY_ID": self.aws_access_key_id,
"AWS_SECRET_ACCESS_KEY": self.aws_secret_access_key,
"AWS_DEFAULT_REGION": self.aws_default_region,
}
)
env["RESTIC_REPOSITORY"] = self.restic_repository
env["RESTIC_PASSWORD"] = self.restic_password
env.update(self.env)
backup_cmd = ["restic", "backup", "--verbose"] + backup_dirs
result = subprocess.run(backup_cmd, env=env, capture_output=True, text=True)
@@ -153,63 +167,47 @@ class ResticStorage(Storage):
class Notifier(ABC):
def send(self, html_message: str):
def send(self, title: str, html_message: str) -> None:
raise NotImplementedError()
class TelegramNotifier(Notifier):
TYPE_NAME = "telegram"
class AppriseNotifier(Notifier):
TYPE_NAME = "apprise"
def __init__(self, name: str, params: Dict[str, Any]):
self.name = name
self.telegram_bot_token = str(params.get("telegram_bot_token", ""))
self.telegram_chat_id = str(params.get("telegram_chat_id", ""))
if not all(
[
self.telegram_bot_token,
self.telegram_chat_id,
]
):
self.api_url = str(params.get("api_url", "")).rstrip("/")
self.tag = str(params.get("tag", ""))
if not self.api_url or not self.tag:
raise ValueError(
f"Missing notification configuration values for backend {name}"
)
def send(self, html_message: str):
url = f"https://api.telegram.org/bot{self.telegram_bot_token}/sendMessage"
data = {
"chat_id": self.telegram_chat_id,
"parse_mode": "HTML",
"text": html_message,
def send(self, title: str, html_message: str) -> None:
url = f"{self.api_url}/notify/{self.tag}/"
payload = {
"title": title,
"body": html_message,
"format": "html",
}
response = requests.post(url, data=data, timeout=30)
response = requests.post(url, json=payload, timeout=30)
if response.status_code == 200:
logger.info("Telegram notification sent successfully")
if response.ok:
logger.info("Apprise notification sent successfully")
else:
logger.error(
f"Failed to send Telegram notification: {response.status_code} - {response.text}"
f"Failed to send Apprise notification: {response.status_code} - {response.text}"
)
class BackupManager:
def __init__(
self,
config: Config,
roots: List[Path],
storages: List[Storage],
notifiers: List[Notifier],
):
self.errors: List[str] = []
class ApplicationFinder:
def __init__(self, roots: List[Path]):
self.roots = roots
self.warnings: List[str] = []
self.successful_backups: List[str] = []
self.config = config
self.roots: List[Path] = roots
self.storages = storages
self.notifiers = notifiers
def find_applications(self) -> List[Application]:
"""Get all application directories and their owners."""
"""Discover all applications with their backup scripts and targets."""
applications: List[Application] = []
source_dirs = itertools.chain(*(root.iterdir() for root in self.roots))
@@ -220,32 +218,182 @@ class BackupManager:
try:
stat_info = app_dir.stat()
owner = pwd.getpwuid(stat_info.st_uid).pw_name
applications.append(Application(path=app_dir, owner=owner))
backup_script = self._find_backup_script(app_dir)
backup_targets = self._find_backup_targets(app_dir)
applications.append(
Application(
path=app_dir,
owner=owner,
backup_script=backup_script,
backup_targets=backup_targets,
)
)
except (KeyError, OSError) as e:
logger.warning(f"Could not get owner for {app_dir}: {e}")
applications.sort(key=lambda app: app.path.name)
return applications
def find_backup_script(self, app_dir: str) -> Optional[str]:
"""Find backup script in user's home directory"""
possible_scripts = [
os.path.join(app_dir, "backup.sh"),
os.path.join(app_dir, "backup"),
]
for script_path in possible_scripts:
if os.path.exists(script_path):
# Check if file is executable
def _find_backup_script(self, app_dir: Path) -> Optional[Path]:
"""Find executable backup script in application directory."""
for name in ("backup.sh", "backup"):
script_path = app_dir / name
if script_path.exists():
if os.access(script_path, os.X_OK):
return script_path
else:
logger.warning(
f"Backup script {script_path} exists but is not executable"
)
return None
def run_app_backup(self, script_path: str, app_dir: str, username: str) -> bool:
def _find_backup_targets(self, app_dir: Path) -> List[Path]:
"""Resolve backup target directories for an application."""
targets_file = app_dir / BACKUP_TARGETS_FILE
resolved_targets: List[Path] = []
if targets_file.exists():
for target_line in self._parse_targets_file(targets_file):
target_path = Path(target_line)
if not target_path.is_absolute():
target_path = (app_dir / target_path).resolve()
else:
target_path = target_path.resolve()
if target_path.exists():
resolved_targets.append(target_path)
else:
warning_msg = (
f"Backup target does not exist for {app_dir}: {target_path}"
)
logger.warning(warning_msg)
self.warnings.append(warning_msg)
else:
default_target = (app_dir / BACKUP_DEFAULT_DIR).resolve()
if default_target.exists():
resolved_targets.append(default_target)
else:
warning_msg = f"Default backup path does not exist for {app_dir}: {default_target}"
logger.warning(warning_msg)
self.warnings.append(warning_msg)
return resolved_targets
def _parse_targets_file(self, targets_file: Path) -> List[str]:
"""Parse backup-targets file, skipping comments and empty lines."""
targets: List[str] = []
try:
for raw_line in targets_file.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
targets.append(line)
except OSError as e:
warning_msg = f"Could not read backup targets file {targets_file}: {e}"
logger.warning(warning_msg)
self.warnings.append(warning_msg)
return targets
class BackupManager:
def __init__(
self,
config: Config,
storages: List[Storage],
notifiers: List[Notifier],
):
self.errors: List[str] = []
self.warnings: List[str] = []
self.successful_backups: List[str] = []
self.config = config
self.storages = storages
self.notifiers = notifiers
self.archive_duration: float = 0.0
self.storage_results: List[StorageRunResult] = []
def run_backup_process(self, applications: List[Application]) -> bool:
"""Main backup process"""
logger.info("Starting backup process")
logger.info(f"Found {len(applications)} application directories")
archive_start = time.monotonic()
# Process each user's backup
for app in applications:
app_dir = str(app.path)
username = app.owner
logger.info(f"Processing backup for app: {app_dir} (user {username})")
if app.backup_script is None:
warning_msg = (
f"No backup script found for app: {app_dir} (user {username})"
)
logger.warning(warning_msg)
self.warnings.append(warning_msg)
continue
self._run_app_backup(str(app.backup_script), app_dir, username)
self.archive_duration = time.monotonic() - archive_start
logger.info(
"Archive phase finished in %s", format_duration(self.archive_duration)
)
# Collect backup directories from applications
backup_dirs: List[str] = []
for app in applications:
for target in app.backup_targets:
target_str = str(target)
if target_str not in backup_dirs:
backup_dirs.append(target_str)
logger.info(f"Found backup directories: {backup_dirs}")
overall_success = True
# Each storage is processed independently: a failure in one storage
# must not prevent the others from being attempted.
for storage in self.storages:
storage_start = time.monotonic()
try:
backup_result = storage.backup(backup_dirs)
except Exception as exc: # noqa: BLE001
logger.error(
"Storage '%s' raised an unexpected error: %s", storage.name, exc
)
backup_result = False
storage_duration = time.monotonic() - storage_start
self.storage_results.append(
StorageRunResult(
name=storage.name,
success=backup_result,
duration=storage_duration,
)
)
logger.info(
"Storage '%s' finished in %s (success=%s)",
storage.name,
format_duration(storage_duration),
backup_result,
)
if not backup_result:
self.errors.append(f"Storage '{storage.name}' backup failed")
# Determine overall success
overall_success = overall_success and backup_result
# Send notification
self._send_notification(overall_success)
logger.info("Backup process completed")
if self.errors:
logger.error(f"Backup completed with {len(self.errors)} errors")
return False
elif self.warnings:
logger.warning(f"Backup completed with {len(self.warnings)} warnings")
return True
else:
logger.info("Backup completed successfully")
return True
def _run_app_backup(self, script_path: str, app_dir: str, username: str) -> bool:
"""Run backup script as the specified user"""
try:
logger.info(f"Running backup script {script_path} (user {username})")
@@ -284,149 +432,51 @@ class BackupManager:
self.errors.append(f"App {username}: {error_msg}")
return False
def get_backup_directories(self) -> List[str]:
"""Collect backup targets according to backup-targets rules"""
backup_dirs: List[str] = []
applications = self.find_applications()
def parse_targets_file(targets_file: Path) -> List[str]:
"""Parse backup-targets file, skipping comments and empty lines."""
targets: List[str] = []
try:
for raw_line in targets_file.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
targets.append(line)
except OSError as e:
warning_msg = f"Could not read backup targets file {targets_file}: {e}"
logger.warning(warning_msg)
self.warnings.append(warning_msg)
return targets
for app in applications:
app_dir = app.path
targets_file = app_dir / BACKUP_TARGETS_FILE
resolved_targets: List[Path] = []
if targets_file.exists():
# Read custom targets defined by the application.
for target_line in parse_targets_file(targets_file):
target_path = Path(target_line)
if not target_path.is_absolute():
target_path = (app_dir / target_path).resolve()
else:
target_path = target_path.resolve()
if target_path.exists():
resolved_targets.append(target_path)
else:
warning_msg = (
f"Backup target does not exist for {app_dir}: {target_path}"
)
logger.warning(warning_msg)
self.warnings.append(warning_msg)
else:
# Fallback to default backups directory when no list is provided.
default_target = (app_dir / BACKUP_DEFAULT_DIR).resolve()
if default_target.exists():
resolved_targets.append(default_target)
else:
warning_msg = f"Default backup path does not exist for {app_dir}: {default_target}"
logger.warning(warning_msg)
self.warnings.append(warning_msg)
for target in resolved_targets:
target_str = str(target)
if target_str not in backup_dirs:
backup_dirs.append(target_str)
return backup_dirs
def send_notification(self, success: bool) -> None:
def _send_notification(self, success: bool) -> None:
"""Send notification to Notifiers"""
host = self.config.host_name
if success and not self.errors:
message = f"<b>{self.config.host_name}</b>: бекап успешно завершен!"
title = f"{host}: бекап успешно завершен"
message = f"<p><b>{host}</b>: бекап успешно завершен!</p>"
if self.successful_backups:
message += f"\n\nУспешные бекапы: {', '.join(self.successful_backups)}"
items = "".join(f"<li>{b}</li>" for b in self.successful_backups)
message += f"<p>Успешные бекапы:</p><ul>{items}</ul>"
else:
message = f"<b>{self.config.host_name}</b>: бекап завершен с ошибками!"
title = f"{host}: бекап завершен с ошибками ({len(self.errors)})"
message = f"<p><b>{host}</b>: бекап завершен с ошибками!</p>"
if self.successful_backups:
message += (
f"\n\n✅ Успешные бекапы: {', '.join(self.successful_backups)}"
)
items = "".join(f"<li>{b}</li>" for b in self.successful_backups)
message += f"<p>✅ Успешные бекапы:</p><ul>{items}</ul>"
if self.warnings:
message += f"\n\n⚠️ Предупреждения:\n" + "\n".join(self.warnings)
items = "".join(f"<li>{w}</li>" for w in self.warnings)
message += f"<p>⚠️ Предупреждения:</p><ul>{items}</ul>"
if self.errors:
message += f"\n\n❌ Ошибки:\n" + "\n".join(self.errors)
items = "".join(f"<li>{e}</li>" for e in self.errors)
message += f"<p>❌ Ошибки:</p><ul>{items}</ul>"
message += f"<p>⏱ Время архивации: {format_duration(self.archive_duration)}</p>"
if self.storage_results:
items = "".join(
f"<li>{'' if r.success else ''} {r.name}: {format_duration(r.duration)}</li>"
for r in self.storage_results
)
message += f"<p>⏱ Время записи в хранилища:</p><ul>{items}</ul>"
for notificator in self.notifiers:
try:
notificator.send(message)
notificator.send(title, message)
except Exception as e:
logger.error(f"Failed to send notification: {str(e)}")
def run_backup_process(self) -> bool:
"""Main backup process"""
logger.info("Starting backup process")
# Get all home directories
applications = self.find_applications()
logger.info(f"Found {len(applications)} application directories")
# Process each user's backup
for app in applications:
app_dir = str(app.path)
username = app.owner
logger.info(f"Processing backup for app: {app_dir} (user {username})")
# Find backup script
backup_script = self.find_backup_script(app_dir)
if backup_script is None:
warning_msg = (
f"No backup script found for app: {app_dir} (user {username})"
)
logger.warning(warning_msg)
self.warnings.append(warning_msg)
continue
self.run_app_backup(backup_script, app_dir, username)
# Get backup directories
backup_dirs = self.get_backup_directories()
logger.info(f"Found backup directories: {backup_dirs}")
overall_success = True
for storage in self.storages:
backup_result = storage.backup(backup_dirs)
if not backup_result:
self.errors.append("Restic backup failed")
# Determine overall success
overall_success = overall_success and backup_result
# Send notification
self.send_notification(overall_success)
logger.info("Backup process completed")
if self.errors:
logger.error(f"Backup completed with {len(self.errors)} errors")
return False
elif self.warnings:
logger.warning(f"Backup completed with {len(self.warnings)} warnings")
return True
else:
logger.info("Backup completed successfully")
return True
def initialize(config_path: Path) -> BackupManager:
def initialize(
config_path: Path,
) -> tuple[ApplicationFinder, BackupManager]:
try:
with config_path.open("rb") as config_file:
raw_config = tomllib.load(config_file)
@@ -458,22 +508,26 @@ def initialize(config_path: Path) -> BackupManager:
if not isinstance(params, dict):
raise ValueError(f"Notificator config for {name} must be a table")
notifier_type = params.get("type", "")
if notifier_type == TelegramNotifier.TYPE_NAME:
notifiers.append(TelegramNotifier(name, params))
if notifier_type == AppriseNotifier.TYPE_NAME:
notifiers.append(AppriseNotifier(name, params))
if not notifiers:
raise ValueError("At least one notification backend must be configured")
config = Config(host_name=host_name, roots=roots)
return BackupManager(
config=config, roots=roots, storages=storages, notifiers=notifiers
config = Config(host_name=host_name)
app_finder = ApplicationFinder(roots)
backup_manager = BackupManager(
config=config, storages=storages, notifiers=notifiers
)
return app_finder, backup_manager
def main():
def main() -> None:
try:
backup_manager = initialize(CONFIG_PATH)
success = backup_manager.run_backup_process()
app_finder, backup_manager = initialize(CONFIG_PATH)
applications = app_finder.find_applications()
backup_manager.warnings.extend(app_finder.warnings)
success = backup_manager.run_backup_process(applications)
if not success:
sys.exit(1)
except KeyboardInterrupt:
+18 -8
View File
@@ -1,4 +1,4 @@
host_name = "{{ notifications_name }}"
host_name = "{{ host_name }}"
roots = [
"{{ application_dir }}"
@@ -8,11 +8,21 @@ roots = [
type = "restic"
restic_repository = "{{ restic_repository }}"
restic_password = "{{ restic_password }}"
aws_access_key_id = "{{ restic_s3_access_key }}"
aws_secret_access_key = "{{ restic_s3_access_secret }}"
aws_default_region = "{{ restic_s3_region }}"
[notifier.server_notifications_channel]
type = "telegram"
telegram_bot_token = "{{ notifications_tg_bot_token }}"
telegram_chat_id = "{{ notifications_tg_chat_id }}"
[storage.yandex_cloud_s3.env]
AWS_ACCESS_KEY_ID = "{{ restic_s3_access_key }}"
AWS_SECRET_ACCESS_KEY = "{{ restic_s3_access_secret }}"
AWS_DEFAULT_REGION = "{{ restic_s3_region }}"
[storage.pr86keedav]
type = "restic"
restic_repository = "{{ restic_pr86keedav_repository }}"
restic_password = "{{ restic_pr86keedav_password }}"
[storage.pr86keedav.env]
RCLONE_CONFIG = "{{ rclone_config_file }}"
[notifier.apprise]
type = "apprise"
api_url = "{{ apprise_external_url }}"
tag = "server"
+6
View File
@@ -0,0 +1,6 @@
[pr86keedav]
type = webdav
url = {{ rclone_pr86keedav_url }}
vendor = other
user = {{ rclone_pr86keedav_user }}
pass = {{ rclone_pr86keedav_password }}
-136
View File
@@ -1,136 +0,0 @@
# -------------------------------------------------------------------
# Global options
# -------------------------------------------------------------------
{
grace_period 15s
admin :2019
# Enable metrics in Prometheus format
# https://caddyserver.com/docs/metrics
metrics
}
# -------------------------------------------------------------------
# Applications
# -------------------------------------------------------------------
vakhrushev.me {
tls anwinged@ya.ru
reverse_proxy {
to homepage_app:80
}
}
auth.vakhrushev.me {
tls anwinged@ya.ru
reverse_proxy authelia_app:9091
}
status.vakhrushev.me, :29999 {
tls anwinged@ya.ru
forward_auth authelia_app:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
reverse_proxy netdata:19999
}
git.vakhrushev.me {
tls anwinged@ya.ru
reverse_proxy {
to gitea_app:3000
}
}
outline.vakhrushev.me {
tls anwinged@ya.ru
reverse_proxy {
to outline_app:3000
}
}
gramps.vakhrushev.me {
tls anwinged@ya.ru
reverse_proxy {
to gramps_app:5000
}
}
miniflux.vakhrushev.me {
tls anwinged@ya.ru
reverse_proxy {
to miniflux_app:8080
}
}
wakapi.vakhrushev.me {
tls anwinged@ya.ru
reverse_proxy {
to wakapi_app:3000
}
}
wanderer.vakhrushev.me {
tls anwinged@ya.ru
reverse_proxy {
to wanderer_web:3000
}
}
memos.vakhrushev.me {
tls anwinged@ya.ru
reverse_proxy {
to memos_app:5230
}
}
wanderbase.vakhrushev.me {
tls anwinged@ya.ru
forward_auth authelia_app:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
reverse_proxy {
to wanderer_db:8090
}
}
rssbridge.vakhrushev.me {
tls anwinged@ya.ru
forward_auth authelia_app:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
reverse_proxy {
to rssbridge_app:80
}
}
dozzle.vakhrushev.me {
tls anwinged@ya.ru
forward_auth authelia_app:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name Remote-Filter
}
reverse_proxy dozzle_app:8080
}
+235
View File
@@ -0,0 +1,235 @@
# -------------------------------------------------------------------
# Global options
# -------------------------------------------------------------------
{
grace_period 15s
admin :2019
# Enable metrics in Prometheus format
# https://caddyserver.com/docs/metrics
metrics
}
# -------------------------------------------------------------------
# Snippets
# -------------------------------------------------------------------
# Shared access log for all sites; consumed by GoAccess.
# Mode 644 lets read-only consumers (goaccess and ad-hoc host-side tail)
# read the file; lumberjack would otherwise default to 0600.
(access_log) {
log {
output file /var/log/caddy/access.log {
mode 644
roll_size 100mib
roll_keep 10
roll_keep_for 720h
}
format json
}
}
# -------------------------------------------------------------------
# Applications
# -------------------------------------------------------------------
vakhrushev.me {
tls anwinged@ya.ru
import access_log
# Matrix federation delegation: tells other servers/clients that the
# homeserver for vakhrushev.me lives at matrix.vakhrushev.me.
# https://spec.matrix.org/latest/server-server-api/#server-discovery
handle /.well-known/matrix/server {
header Content-Type application/json
header Access-Control-Allow-Origin *
respond `{"m.server": "matrix.vakhrushev.me:443"}`
}
handle /.well-known/matrix/client {
header Content-Type application/json
header Access-Control-Allow-Origin *
respond `{"m.homeserver": {"base_url": "https://matrix.vakhrushev.me"}}`
}
handle {
reverse_proxy {
to homepage_app:80
}
}
}
matrix.vakhrushev.me {
tls anwinged@ya.ru
import access_log
reverse_proxy {
to tuwunel_app:6167
}
}
auth.vakhrushev.me {
tls anwinged@ya.ru
import access_log
reverse_proxy authelia_app:9091
}
status.vakhrushev.me, :29999 {
tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
reverse_proxy netdata:19999
}
git.vakhrushev.me {
tls anwinged@ya.ru
import access_log
reverse_proxy {
to gitea_app:3000
}
}
outline.vakhrushev.me {
tls anwinged@ya.ru
import access_log
reverse_proxy {
to outline_app:3000
}
}
gramps.vakhrushev.me {
tls anwinged@ya.ru
import access_log
reverse_proxy {
to gramps_app:5000
}
}
miniflux.vakhrushev.me {
tls anwinged@ya.ru
import access_log
reverse_proxy {
to miniflux_app:8080
}
}
wakapi.vakhrushev.me {
tls anwinged@ya.ru
import access_log
reverse_proxy {
to wakapi_app:3000
}
}
wanderer.vakhrushev.me {
tls anwinged@ya.ru
import access_log
reverse_proxy {
to wanderer_web:3000
}
}
memos.vakhrushev.me {
tls anwinged@ya.ru
import access_log
reverse_proxy {
to memos_app:5230
}
}
remembos.vakhrushev.me {
tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
reverse_proxy {
to remembos_app:8080
}
}
calibre.vakhrushev.me {
tls anwinged@ya.ru
import access_log
reverse_proxy {
to calibre_web_app:8083
}
}
wanderbase.vakhrushev.me {
tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
reverse_proxy {
to wanderer_db:8090
}
}
rssbridge.vakhrushev.me {
tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
reverse_proxy {
to rssbridge_app:80
}
}
dozzle.vakhrushev.me {
tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name Remote-Filter
}
reverse_proxy dozzle_app:8080
}
goaccess.vakhrushev.me {
tls anwinged@ya.ru
import access_log
forward_auth authelia_app:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
@websocket {
header Connection *Upgrade*
header Upgrade websocket
}
reverse_proxy @websocket goaccess_processor:7890
reverse_proxy goaccess_app:8080
}
@@ -0,0 +1,23 @@
services:
caddyproxy:
image: caddy:2.11.2
restart: unless-stopped
container_name: "caddyproxy"
ports:
- "80:80"
- "443:443"
- "443:443/udp"
cap_add:
- NET_ADMIN
volumes:
- "{{ caddy_file_dir }}:/etc/caddy"
- "{{ data_dir }}:/data"
- "{{ config_dir }}:/config"
- "{{ caddy_logs_dir }}:/var/log/caddy"
networks:
- "web_proxy_network"
networks:
web_proxy_network:
external: true
-22
View File
@@ -1,22 +0,0 @@
services:
{{ service_name }}:
image: caddy:2.10.2
restart: unless-stopped
container_name: {{ service_name }}
ports:
- "80:80"
- "443:443"
- "443:443/udp"
cap_add:
- NET_ADMIN
volumes:
- {{ caddy_file_dir }}:/etc/caddy
- {{ data_dir }}:/data
- {{ config_dir }}:/config
networks:
- "web_proxy_network"
networks:
web_proxy_network:
external: true
+23
View File
@@ -0,0 +1,23 @@
services:
calibre_web_app:
image: lscr.io/linuxserver/calibre-web:0.6.26
container_name: calibre_web_app
restart: unless-stopped
networks:
- "web_proxy_network"
volumes:
- "{{ config_dir }}:/config"
- "{{ books_dir }}:/books:ro"
environment:
- "PUID={{ owner_create_result.uid }}"
- "PGID={{ owner_create_result.group }}"
- TZ=Etc/UTC
# - DOCKER_MODS=linuxserver/mods:universal-calibre #optional
# - OAUTHLIB_RELAX_TOKEN_SCOPE=1 #optional
# ports:
# - 8083:8083
networks:
web_proxy_network:
external: true
+1 -1
View File
@@ -1,7 +1,7 @@
services:
dozzle_app:
image: amir20/dozzle:v8.14.11
image: amir20/dozzle:v10.6.0
container_name: dozzle_app
restart: unless-stopped
volumes:
@@ -7,7 +7,7 @@ echo "Gitea: backup data with gitea dump"
(cd "{{ base_dir }}" && \
docker compose exec \
-u "{{ user_create_result.uid }}:{{ user_create_result.group }}" \
-u "{{ owner_create_result.uid }}:{{ owner_create_result.group }}" \
-w /backups gitea_app \
gitea dump -c /data/gitea/conf/app.ini \
)
@@ -1,21 +1,21 @@
services:
gitea_app:
image: gitea/gitea:1.25.3
image: gitea/gitea:1.26.1
restart: unless-stopped
container_name: gitea_app
ports:
- "2222:22"
volumes:
- {{ data_dir }}:/data
- {{ backups_dir }}:/backups
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- "{{ data_dir }}:/data"
- "{{ backups_dir }}:/backups"
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
networks:
- "web_proxy_network"
environment:
- "USER_UID={{ user_create_result.uid }}"
- "USER_GID={{ user_create_result.group }}"
- "USER_UID={{ owner_create_result.uid }}"
- "USER_GID={{ owner_create_result.group }}"
- "GITEA__server__SSH_PORT=2222"
# Mailer
+8
View File
@@ -0,0 +1,8 @@
FROM allinurl/goaccess:1.10.2
RUN apk add --no-cache jq
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod 0755 /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -0,0 +1,41 @@
services:
goaccess_processor:
build: .
image: local/goaccess-jq:1.10.2
container_name: goaccess_processor
restart: unless-stopped
init: true
user: "{{ app_owner_uid }}:{{ app_owner_gid }}"
command:
- --log-format=COMBINED
- --enable-panel=VIRTUAL_HOSTS
- --real-time-html
- --port=7890
- --ws-url=wss://goaccess.vakhrushev.me:443
- --output=/srv/report/index.html
- --persist
- --restore
- --db-path=/srv/db
- --no-global-config
volumes:
- "{{ caddy_logs_dir }}:/srv/logs:ro"
- "{{ db_dir }}:/srv/db"
- "{{ report_dir }}:/srv/report"
networks:
- "web_proxy_network"
goaccess_app:
image: caddy:2.11.2
container_name: goaccess_app
restart: unless-stopped
user: "{{ app_owner_uid }}:{{ app_owner_gid }}"
command: caddy file-server --listen :8080 --root /srv --browse
volumes:
- "{{ report_dir }}:/srv:ro"
networks:
- "web_proxy_network"
networks:
web_proxy_network:
external: true
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
# Tail Caddy's JSON access log, transform each entry into Apache CLF
# Combined with the virtual host glued to the request URI, and feed
# the stream straight into goaccess via stdin. Result: every line in
# the Requests panel renders as `host.example.com/path`.
set -eu
ACCESS_LOG="/srv/logs/access.log"
JQ_FILTER='
"\(.request.remote_ip // "-") - - " +
"[\((.ts // 0) | gmtime | strftime("%d/%b/%Y:%H:%M:%S +0000"))] " +
"\"\(.request.method) \(.request.host)\(.request.uri) \(.request.proto)\" " +
"\(.status) \(.size) " +
"\"\(.request.headers.Referer[0]? // "-")\" " +
"\"\(.request.headers["User-Agent"][0]? // "-")\""
'
tail -F -n +1 "$ACCESS_LOG" \
| jq --unbuffered -rc "$JQ_FILTER" \
| exec goaccess - "$@"
+1 -1
View File
@@ -3,7 +3,7 @@
services:
gramps_app: &gramps_app
image: ghcr.io/gramps-project/grampsweb:25.11.2
image: ghcr.io/gramps-project/grampsweb:26.5.1
container_name: gramps_app
depends_on:
- gramps_redis
Executable → Regular
+6 -2
View File
@@ -9,7 +9,9 @@ def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Rename Gramps document files by appending extensions from a list."
)
parser.add_argument("directory", type=Path, help="Directory containing hashed files")
parser.add_argument(
"directory", type=Path, help="Directory containing hashed files"
)
parser.add_argument("names_file", type=Path, help="Text file with target names")
return parser.parse_args()
@@ -33,7 +35,9 @@ def rename_files(directory: Path, names: list[str]) -> None:
for name in names:
hash_part, dot, _ = name.partition(".")
if not dot:
print(f"Skipping invalid entry (missing extension): {name}", file=sys.stderr)
print(
f"Skipping invalid entry (missing extension): {name}", file=sys.stderr
)
continue
source = directory / hash_part
+2 -2
View File
@@ -4,7 +4,7 @@ import os
import argparse
def main():
def main() -> None:
parser = argparse.ArgumentParser(
description="Retain specified number of files in a directory sorted by name, delete others."
)
@@ -32,7 +32,7 @@ def main():
sorted_files = sorted(files)
# Identify files to delete
to_delete = sorted_files[:-args.keep] if args.keep > 0 else sorted_files.copy()
to_delete = sorted_files[: -args.keep] if args.keep > 0 else sorted_files.copy()
# Delete files and print results
for filename in to_delete:
+2 -2
View File
@@ -3,10 +3,10 @@
services:
memos_app:
image: neosmemo/memos:0.25.3
image: neosmemo/memos:0.28.0
container_name: memos_app
restart: unless-stopped
user: "{{ user_create_result.uid }}:{{ user_create_result.group }}"
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
networks:
- "web_proxy_network"
volumes:
+9 -4
View File
@@ -5,7 +5,7 @@ services:
miniflux_app:
image: miniflux/miniflux:2.2.10
container_name: miniflux_app
user: "{{ user_create_result.uid }}:{{ user_create_result.group }}"
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
depends_on:
miniflux_postgres:
condition: service_healthy
@@ -32,11 +32,11 @@ services:
- OAUTH2_USER_CREATION=1
- METRICS_COLLECTOR=1
- METRICS_ALLOWED_NETWORKS=0.0.0.0/0
miniflux_postgres:
image: postgres:16.3-bookworm
container_name: miniflux_postgres
user: "{{ user_create_result.uid }}:{{ user_create_result.group }}"
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
restart: 'unless-stopped'
environment:
- POSTGRES_USER={{ miniflux_postgres_user }}
@@ -50,7 +50,12 @@ services:
- "{{ secrets_dir }}:/secrets:ro"
- "{{ postgres_data_dir }}:/var/lib/postgresql/data"
healthcheck:
test: ["CMD", "pg_isready", "--username={{ miniflux_postgres_user }}", "--dbname={{ miniflux_postgres_database }}"]
test: [
"CMD",
"pg_isready",
"--username={{ miniflux_postgres_user }}",
"--dbname={{ miniflux_postgres_database }}",
]
interval: 10s
start_period: 30s
+1 -1
View File
@@ -1,7 +1,7 @@
services:
netdata:
image: netdata/netdata:v2.8.4
image: netdata/netdata:v2.10.3
container_name: netdata
restart: unless-stopped
cap_add:
@@ -0,0 +1,67 @@
# Resource alerts for a low-spec home server.
# Overrides stock alerts where thresholds differ; baseline RAM use is ~80%, so stock 80/90% would fire constantly.
# RAM: warn at >92%, crit at >95% — by then less than ~200 MB free.
alarm: ram_in_use
on: system.ram
class: Utilization
type: System
component: Memory
calc: $used * 100 / ($used + $cached + $free + $buffers)
units: %
every: 10s
warn: $this > 92
crit: $this > 95
delay: down 5m multiplier 1.5 max 1h
summary: System memory utilization
info: System memory utilization (used / total, excluding reclaimable cache)
to: sysadmin
# CPU: replace stock 10min_cpu_usage with two windowed alerts.
alarm: 10min_cpu_usage
on: system.cpu
enabled: no
alarm: cpu_warn_30m
on: system.cpu
class: Utilization
type: System
component: CPU
lookup: average -30m unaligned of user,system,softirq,irq,guest,guest_nice,nice
units: %
every: 1m
warn: $this > 80
delay: down 30m multiplier 1.5 max 2h
summary: Sustained CPU load (30m avg)
info: Average CPU utilization over the last 30 minutes
to: sysadmin
alarm: cpu_crit_15m
on: system.cpu
class: Utilization
type: System
component: CPU
lookup: average -15m unaligned of user,system,softirq,irq,guest,guest_nice,nice
units: %
every: 1m
crit: $this > 95
delay: down 30m multiplier 1.5 max 2h
summary: High CPU load (15m avg)
info: Average CPU utilization over the last 15 minutes
to: sysadmin
# Disk: warn at >75%, crit at >90% on every mounted filesystem.
template: disk_space_usage
on: disk.space
class: Utilization
type: System
component: Disk
calc: $used * 100 / ($avail + $used)
units: %
every: 1m
warn: $this > 75
crit: $this > 90
delay: down 15m multiplier 1.5 max 1h
summary: Disk space utilization
info: Disk space utilization on ${label:mount_point}
to: sysadmin
@@ -0,0 +1,45 @@
# Override stock health_alarm_notify.conf — route every alert to apprise.
# Stock conf is sourced first; this only sets what differs.
SEND_EMAIL="NO"
SEND_CUSTOM="YES"
DEFAULT_RECIPIENT_CUSTOM="server"
role_recipients_custom[sysadmin]="server"
role_recipients_custom[domainadmin]="server"
role_recipients_custom[dba]="server"
role_recipients_custom[webmaster]="server"
role_recipients_custom[proxyadmin]="server"
role_recipients_custom[silent]=""
custom_sender() {
local apprise_url="http://apprise:8000/notify/${1}/"
local notif_type="info"
case "${status}" in
CRITICAL) notif_type="failure" ;;
WARNING) notif_type="warning" ;;
CLEAR) notif_type="success" ;;
esac
local title="[${status}] ${name} on ${host}"
local body="${status_message}: ${alarm}
Chart: ${chart}
Value: ${value} ${units}
Info: ${info}
Raised for: ${raised_for}"
local httpcode
httpcode=$(docurl -X POST \
--data-urlencode "title=${title}" \
--data-urlencode "body=${body}" \
--data-urlencode "type=${notif_type}" \
"${apprise_url}")
if [ "${httpcode}" = "200" ]; then
info "sent custom notification for ${name} on ${host}"
return 0
fi
error "failed to send notification for ${name} on ${host} (HTTP ${httpcode})"
return 1
}
+7 -12
View File
@@ -3,8 +3,9 @@ services:
# See sample https://github.com/outline/outline/blob/main/.env.sample
outline_app:
image: outlinewiki/outline:1.1.0
image: outlinewiki/outline:1.7.1
container_name: outline_app
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
restart: unless-stopped
depends_on:
- outline_postgres
@@ -12,6 +13,8 @@ services:
networks:
- "outline_network"
- "web_proxy_network"
volumes:
- "{{ media_dir }}:/var/lib/outline/data"
environment:
NODE_ENV: 'production'
URL: 'https://outline.vakhrushev.me'
@@ -22,16 +25,8 @@ services:
PGSSLMODE: 'disable'
REDIS_URL: 'redis://outline_redis:6379'
FILE_STORAGE: 's3'
FILE_STORAGE_UPLOAD_MAX_SIZE: '262144000'
AWS_ACCESS_KEY_ID: '{{ outline_s3_access_key }}'
AWS_SECRET_ACCESS_KEY: '{{ outline_s3_secret_key }}'
AWS_REGION: '{{ outline_s3_region }}'
AWS_S3_ACCELERATE_URL: ''
AWS_S3_UPLOAD_BUCKET_URL: '{{ outline_s3_url }}'
AWS_S3_UPLOAD_BUCKET_NAME: '{{ outline_s3_bucket }}'
AWS_S3_FORCE_PATH_STYLE: 'true'
AWS_S3_ACL: 'private'
FILE_STORAGE: 'local'
FILE_STORAGE_UPLOAD_MAX_SIZE: '262144000' # 250 MB
OIDC_CLIENT_ID: '{{ outline_oidc_client_id | replace("$", "$$") }}'
OIDC_CLIENT_SECRET: '{{ outline_oidc_client_secret | replace("$", "$$") }}'
@@ -62,7 +57,7 @@ services:
outline_postgres:
image: postgres:16.3-bookworm
container_name: outline_postgres
user: "{{ user_create_result.uid }}:{{ user_create_result.group }}"
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
restart: unless-stopped
volumes:
- "/etc/passwd:/etc/passwd:ro"
+125
View File
@@ -0,0 +1,125 @@
# =============================================================================
# Remembos — конфигурация
# =============================================================================
# Скопируйте этот файл в config.toml и заполните значения.
# =============================================================================
# Подключение к Memos
# =============================================================================
[memos]
# Адрес инстанса Memos, включая протокол.
# Пример: "https://memos.example.com"
url = "http://memos_app:5230"
# Токен доступа к API.
# Создаётся в Memos: Settings → Access Tokens.
token = "{{ remembos_memos_token }}"
# Публичный адрес Memos (для ссылок на оригинальные заметки).
# Если Memos и Remembos развёрнуты на одном сервере, внутренний адрес (url)
# может отличаться от публичного. Если не указан — используется url.
# Пример: "https://memos.example.com"
public_url = "https://memos.vakhrushev.me"
# =============================================================================
# База данных (SQLite)
# =============================================================================
[database]
# Путь к файлу базы данных SQLite.
# В ней хранится история показов и кэш запросов.
# Если файл не существует — будет создан автоматически.
path = "/data/remembos.db"
# =============================================================================
# Алгоритм поиска воспоминаний
# Подробное описание: spec/SEARCH.md
# =============================================================================
[search]
# Минимальное количество дней, прежде чем одна и та же заметка
# может быть показана повторно. Чем больше значение, тем реже
# будут повторяться воспоминания, но при малом количестве заметок
# это может привести к ситуации, когда нечего показать.
cooldown_days = 90
# Ослабленный cooldown: используется как fallback, если с основным
# cooldown не удалось найти ни одного кандидата.
relaxed_cooldown_days = 30
# Максимальное количество заметок, запрашиваемых у Memos за один запрос.
# Влияет на размер пула кандидатов внутри каждого уровня поиска.
page_size = 50
# Насколько далеко в прошлое искать воспоминания (в годах).
# Например, 10 означает поиск заметок за последние 10 лет.
max_years_back = 10
# Предпочитать более старые воспоминания при выборе из кандидатов.
# Если true — заметки из далёкого прошлого получают более высокий вес.
# Если false — все кандидаты внутри уровня равноценны.
prefer_older = true
# Веса уровней поиска (в процентах, сумма должна быть 100).
# Определяют вероятность выбора каждого уровня при поиске.
#
# Tier 1 — точная дата в прошлые годы (самое ценное совпадение)
# Tier 2 — тот же день месяца в прошлые месяцы
# Tier 3 — та же неделя (±3 дня) в прошлые годы
# Tier 4 — тот же месяц в прошлые годы
# Tier 5 — тот же квартал в прошлые годы
# Tier 6 — то же полугодие в прошлые годы
# Tier 7 — недавнее прошлое (от 2 до 6 месяцев назад)
[search.tier_weights]
tier1 = 35
tier2 = 15
tier3 = 15
tier4 = 12
tier5 = 10
tier6 = 5
tier7 = 8
# =============================================================================
# Telegram-бот
# =============================================================================
[telegram]
# Включить Telegram-бот для ежедневной отправки воспоминаний.
enabled = true
# Токен бота, полученный от @BotFather.
token = "{{ remembos_telegram_token }}"
# ID чата, в который бот отправляет воспоминания.
# Можно узнать через @userinfobot или из логов бота при первом сообщении.
chat_id = {{ remembos_telegram_chat_id }}
# Время ежедневной отправки воспоминания (формат HH:MM).
# Используется часовой пояс, указанный в параметре timezone.
send_at = "09:00"
# =============================================================================
# Веб-приложение
# =============================================================================
[web]
# Адрес и порт, на котором запускается веб-сервер.
listen = "0.0.0.0:8080"
# =============================================================================
# Общие настройки
# =============================================================================
[general]
# Часовой пояс для определения «сегодняшнего дня» и времени отправки.
# Формат — IANA (например, "Europe/Moscow", "Asia/Yekaterinburg").
timezone = "Europe/Moscow"
# Уровень логирования: debug, info, warn, error.
log_level = "info"
# Разрешить загрузку дополнительных воспоминаний (кнопка на веб-странице, /more в Telegram).
# Полезно для тестирования. Каждое загруженное воспоминание записывается в историю показов.
allow_load_more = true
@@ -0,0 +1,22 @@
services:
remembos_app:
image: "{{ yc_container_registry_repository }}/remembos:v0.2.0"
container_name: remembos_app
restart: unless-stopped
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
environment:
- PUID={{ owner_create_result.uid }}
- PGID={{ owner_create_result.group }}
networks:
- "web_proxy_network"
volumes:
- "{{ config_dir }}:/config:ro"
- "{{ data_dir }}:/data"
command:
- "--config"
- "/config/config.toml"
networks:
web_proxy_network:
external: true
@@ -1,5 +1,5 @@
services:
rssbridge_app:
image: rssbridge/rss-bridge:2025-08-05
container_name: rssbridge_app
+43 -43
View File
@@ -1,44 +1,44 @@
$ANSIBLE_VAULT;1.1;AES256
33396537353265633634336630353330653337623861373731613734663938633837613437366537
3439383366633266623463366530626662346338393165630a663539313066663061353635666366
61393437393131333166626165306563366661353338363138633239666566313330363331666537
3763356535396334380a386362383436363732353234333033613133383264643934306432313335
34646164323664636532663835306230386633316539373564383163346663376666633564326134
30666135626637343963383766383836653135633739636261353666303666633566346562643962
63376165636434343066306539653637343736323437653465656436323533636237643333326438
35626239323530643066363533323039393237333338316135313838643464306161646635313062
36386565626435373333393566393831366538363864313737306565343162316536353539333864
63376264643566613266373665666363366662643262616634333132386535383731396462633430
32343738343039616139343833366661303430383766376139636434616565356161396433643035
37363165383935373937346464343738643430333764336264373931616332393964346566636638
39303434343461326464623363323937396663376335316237373166306134636432376435663033
34346436623435626363636237373965633139343661623135633764303862353465306235666563
66653764666635636462636434663264646665383236343166643133613966366334653030653262
38326437313939616332636638323033346139343732653933356239306132613665376163646164
30316663643666633334653133613764396165646533636534613931663138666366316235396466
61313964396264626339306135376635633133366433303033633363396132303938363638346333
66326466326134313535393831343262363862663065323135643630316431336531373833316363
64376338653366353031333836643137333736363534363164306331313337353663653961623665
64626562366637336637353433303261303964633236356162363139396339396136393237643935
34316266326561663834353762343766363933313463313263393063343562613933393361653861
38363635323231666438366536626435373365323733663139666534636564623666356436346539
63326436386436356636633637373738343032353664323736653939346234643165313461643833
35666439613136396264313033336539313537613238393262306365656238396464373936616538
64316365616464386638313331653030346330393665353539393834346135643434363736323135
37663433326439356663633162616435313061353662373766633731636439636266666466613363
39343930386534376330663230623832643933336235636166626534366664366562356165373764
63343432323864366162376263656565646661633536666336643030363039616666343063386165
37343238303034313832393538313632396261316232376635633732656663396631323261363433
38373738363833323934353739643538376237316535623035383965613965636337646537326537
64663837643632666334393634323264613139353332306263613165383733386662366333316139
63373839346265366166333331353231663763306163323063613138323835313831303666306561
39316666343761303464333535336361333462623363633333383363303134336139356436666165
62616364373030613837353939363636653537373965613531636130383266643637333233316137
39353866366239643265366162663031346439663234363935353138323739393337313835313062
33373263326565383735366364316461323930336437623834356132346633636364313732383661
66346634613762613037386238656334616430633037343066623463313035646339313638653137
65643166316664626236633332326136303235623934306462643636373437373630346435633835
66346364393236393563623032306631396561623263653236393939313333373635303365316638
66373037333565323733656331636337336665363038353635383531386366633632363031623430
31356461663438653736316464363231303938653932613561633139316361633461626361383132
396436303634613135383839396566393135
64636264303339643062343231346262316463353562396635623734643763376163383361623135
3065326434613532376662343761323339316234356363630a386563613639363332623137653365
30386363303332333566633737306335336162396366316133653837326264383966653762383037
6466333038346436650a343238626637356532376133323464396666643061376363393466663838
37313235336563643361316339316661356639343933396161333335356332363933656530393634
34376632326135373864636232616163373738383165326338613037303364323530313766343038
34366538633062623064626131376261663032666663306339663361663665303866373833646261
32663931626266663064663066643866356532353363636365633139663930353764386436623539
66313061393564303737306261383632303063313032613033336130376563386139353835303531
65623864613639346238663434653361616563626639643437636638396230323232393065663839
34383737653064343433313364663532363635326165623361303536373136666130306266383237
61653939326137356139363535353666356265336536393763656136353661636336633231366132
62303065663233306130316435333364313039366362393762383463313035333034623730643931
63323035633838303530313361323966346437656366386430316631303637376431396261343166
31313734643831376633383065373436633136633261373838633662633433363363616162323233
34666564333637316266623439383934363862336238613436356531373834643262653463326634
64306530383338613161303138313038393433306430663331343033393832323532376261653838
33373463636533356134633030393965386131353034323734303934636462363863386231353534
65303739643338653265313864633632376461373766343536626464303332636332346531303165
64396363323465393736633937363435663662346136613636643265353830616563623838613632
65613565363139323431653463363461353666313464656664656331633263333766353666346138
37366561366262356239366133616266643032636239363238643237383663633433383365626238
61336632613763616439373730633532316362623663646365303336383531633438323837323939
32313962313264303435633736346565326438626238356361353264353666643165653535303336
31633137396465363035373137636162366165323130396631373865393638376335313838396138
37316263663535376664383764343030623138363137356465316664336564636166313163353566
30656636626163333138346639323465396531666664396231326136653430343061393234366266
35386534633131666166353938343066343830613133643833303338656165393439373038336638
34373436313931666234393530353536353866343330616133653563303764363962333361353639
61316365373565313865393364356361313063303333303063623435323336316534643937316466
30633664353131336531336332323862653566363635623965373238313965353434343733303239
34313836353233653333336130376532386265383762383163386264396231623938616162363861
64363938366665626666383033356566623765363737643565643964326135666566383866366563
39643532306134346562346665656431656366383564376135633536313965613738333535376137
31663566336664373436613866396434623133663361343564623535646462366636616236396661
34663866323835373438623533353833663261663736646335316564383339343363626264343630
65346434303763343763383337306463376235663361643037636231323139303239363532303439
34316133326639623035653532346130633263376531623130616239626433343131333064333632
35626531353562396462633639653534373537356666343266396565623137306232656633303335
35316432643633643139616264316636383364316432373533373535323762353035346434343166
39356266643137663365613832313765376462623032366332363563306536353736333461643930
38633666333330323433373532313030316130346464616565333265376533303564303638376536
373863396332313264373733323437303130
@@ -4,7 +4,7 @@ services:
# noinspection ComposeUnknownValues
image: "{{ registry_transcriber_image }}"
container_name: transcriber_app
user: "{{ user_create_result.uid }}:{{ user_create_result.group }}"
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
restart: unless-stopped
volumes:
- "{{ config_file }}:/config/config.toml:ro"
@@ -13,8 +13,8 @@ services:
- "web_proxy_network"
- "monitoring_network"
environment:
- "USER_UID={{ user_create_result.uid }}"
- "USER_GID={{ user_create_result.group }}"
- "USER_UID={{ owner_create_result.uid }}"
- "USER_GID={{ owner_create_result.group }}"
command: ./transcriber --config=/config/config.toml
networks:
+36
View File
@@ -0,0 +1,36 @@
# See versions: https://github.com/matrix-construct/tuwunel/releases
# Configuration reference: https://github.com/matrix-construct/tuwunel/blob/main/tuwunel-example.toml
services:
tuwunel_app:
image: jevolk/tuwunel:v1.6.1
container_name: tuwunel_app
restart: unless-stopped
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
networks:
- "web_proxy_network"
volumes:
- "{{ data_dir }}:/var/lib/tuwunel"
environment:
TUWUNEL_SERVER_NAME: "{{ tuwunel_server_name }}"
TUWUNEL_DATABASE_PATH: "/var/lib/tuwunel"
TUWUNEL_ADDRESS: "0.0.0.0"
TUWUNEL_PORT: "6167"
TUWUNEL_MAX_REQUEST_SIZE: "20000000"
TUWUNEL_ALLOW_REGISTRATION: "false"
TUWUNEL_ALLOW_FEDERATION: "true"
TUWUNEL_ALLOW_CHECK_FOR_UPDATES: "false"
TUWUNEL_TRUSTED_SERVERS: '["matrix.org"]'
# Well-known delegation values returned to clients/servers that query tuwunel directly.
# The canonical delegation is served by Caddy on {{ tuwunel_server_name }} (see Caddyfile).
TUWUNEL_WELL_KNOWN_SERVER: "{{ tuwunel_well_known_server }}"
TUWUNEL_WELL_KNOWN_CLIENT: "{{ tuwunel_well_known_client }}"
TUWUNEL_LOG: "info"
networks:
web_proxy_network:
external: true
@@ -3,10 +3,10 @@
services:
wakapi_app:
image: ghcr.io/muety/wakapi:2.16.1
image: ghcr.io/muety/wakapi:2.17.3
container_name: wakapi_app
restart: unless-stopped
user: '{{ user_create_result.uid }}:{{ user_create_result.group }}'
user: '{{ owner_create_result.uid }}:{{ owner_create_result.group }}'
networks:
- "web_proxy_network"
volumes:
+3 -3
View File
@@ -7,7 +7,7 @@ services:
wanderer_search:
container_name: wanderer_search
image: getmeili/meilisearch:v1.20.0
user: "{{ user_create_result.uid }}:{{ user_create_result.group }}"
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
environment:
<<: *cenv
MEILI_NO_ANALYTICS: "true"
@@ -28,7 +28,7 @@ services:
wanderer_db:
container_name: wanderer_db
image: "flomp/wanderer-db:{{ wanderer_version }}"
user: "{{ user_create_result.uid }}:{{ user_create_result.group }}"
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
depends_on:
wanderer_search:
condition: service_healthy
@@ -54,7 +54,7 @@ services:
wanderer_web:
container_name: wanderer_web
image: "flomp/wanderer-web:{{ wanderer_version }}"
user: "{{ user_create_result.uid }}:{{ user_create_result.group }}"
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
depends_on:
wanderer_search:
condition: service_healthy
-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env sh
# Must be executed for every user
# See https://cloud.yandex.ru/docs/container-registry/tutorials/run-docker-on-vm#run
set -eu
curl --silent --show-error -H Metadata-Flavor:Google 169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token | \
cut -f1 -d',' | \
cut -f2 -d':' | \
tr -d '"' | \
docker login --username iam --password-stdin cr.yandex
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
exec uv run inv "$@"
+23 -9
View File
@@ -9,17 +9,31 @@ templates:
pre-commit:
jobs:
- name: "format python"
glob: "**/*.py"
run: "uv run ruff format {staged_files}"
stage_fixed: true
- name: "check python"
glob: "**/*.py"
run: "uv run ruff check {staged_files}"
- name: "mypy"
glob: "**/*.py"
run: "uv run mypy {staged_files}"
- name: "yamllint"
glob: "**/*.{yml,yaml}"
run: "uv run yamllint --config-file .yamllint.yml --format colored {staged_files}"
- name: "ansible-lint"
glob: "**/*.{yml,yaml}"
exclude:
- ".gitea/**"
run: "uv run ansible-lint --profile production --offline -- {staged_files}"
- name: "gitleaks"
run: "gitleaks git --staged"
- name: "check secret files"
run: "python3 {av-hooks-dir}/pre-commit/check-secrets-encrypted-with-ansible-vault.py"
- name: "format python"
glob: "**/*.py"
run: "black --quiet {staged_files}"
stage_fixed: true
- name: "mypy"
glob: "**/*.py"
run: "mypy {staged_files}"
+15
View File
@@ -7,6 +7,9 @@
- name: 'Configure dozzle'
ansible.builtin.import_playbook: playbook-dozzle.yml
- name: 'Configure goaccess'
ansible.builtin.import_playbook: playbook-goaccess.yml
- name: 'Configure gitea'
ansible.builtin.import_playbook: playbook-gitea.yml
@@ -31,6 +34,18 @@
- name: 'Configure wanderer'
ansible.builtin.import_playbook: playbook-wanderer.yml
- name: 'Configure calibre'
ansible.builtin.import_playbook: playbook-calibre.yml
- name: 'Configure remembos'
ansible.builtin.import_playbook: playbook-remembos.yml
- name: 'Configure apprise'
ansible.builtin.import_playbook: playbook-apprise.yml
- name: 'Configure tuwunel'
ansible.builtin.import_playbook: playbook-tuwunel.yml
#
- name: 'Configure homepage'
+60
View File
@@ -0,0 +1,60 @@
---
- name: "Configure apprise application"
hosts: all
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "apprise"
app_user: "{{ app_name }}"
app_owner_uid: 1104
app_owner_gid: 1104
base_dir: "{{ (application_dir, app_name) | path_join }}"
config_dir: "{{ (base_dir, 'config') | path_join }}"
tasks:
- name: "Create user and environment"
ansible.builtin.import_role:
name: owner
vars:
owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories"
ansible.builtin.file:
path: "{{ item }}"
state: "directory"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ base_dir }}"
- "{{ config_dir }}"
- name: "Copy apprise config"
ansible.builtin.template:
src: "./files/{{ app_name }}/server.template.cfg"
dest: "{{ config_dir }}/server.cfg"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
- name: "Run application with docker compose"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "present"
remove_orphans: true
tags:
- run-app
+1
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
- files/authelia/secrets.yml
vars:
+22 -2
View File
@@ -4,11 +4,15 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
backup_config_dir: "/etc/backup"
backup_config_file: "{{ (backup_config_dir, 'config.toml') | path_join }}"
rclone_config_dir: "/etc/rclone"
rclone_config_file: "{{ (rclone_config_dir, 'rclone.conf') | path_join }}"
restic_shell_script: "{{ (bin_prefix, 'restic-shell.sh') | path_join }}"
backup_all_script: "{{ (bin_prefix, 'backup-all.py') | path_join }}"
@@ -21,6 +25,22 @@
group: root
mode: "0755"
- name: "Create rclone config directory"
ansible.builtin.file:
path: "{{ rclone_config_dir }}"
state: "directory"
owner: root
group: root
mode: "0755"
- name: "Create rclone config file"
ansible.builtin.template:
src: "files/backups/rclone.template.conf"
dest: "{{ rclone_config_file }}"
owner: root
group: root
mode: "0640"
- name: "Create backup config file"
ansible.builtin.template:
src: "files/backups/config.template.toml"
@@ -35,11 +55,11 @@
state: present
line: "{{ primary_user }} ALL=(ALL) NOPASSWD: {{ backup_all_script }}"
validate: /usr/sbin/visudo -cf %s # ВАЖНО: проверка синтаксиса перед сохранением
create: no # Файл уже должен существовать
create: false # Файл уже должен существовать
- name: "Copy restic shell script"
ansible.builtin.template:
src: "files/backups/restic-shell.sh.j2"
src: "files/backups/restic-shell.template.sh"
dest: "{{ restic_shell_script }}"
owner: root
group: root
+32 -2
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "caddyproxy"
@@ -41,9 +42,38 @@
- "{{ config_dir }}"
- "{{ caddy_file_dir }}"
# Shared HTTP access log directory: caddy writes here, other
# containers (goaccess, etc.) mount it read-only. Dir mode 0755
# so anyone can list/read; the file mode itself comes from the
# `mode 644` option in the Caddyfile log snippet.
- name: "Create shared caddy logs directory"
ansible.builtin.file:
path: "{{ caddy_logs_dir }}"
state: "directory"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0755"
- name: "Find pre-existing caddy log files"
ansible.builtin.find:
paths: "{{ caddy_logs_dir }}"
file_type: "file"
register: caddy_log_files
# Lumberjack created earlier files with 0600 before we set `mode`
# in the Caddyfile; relax them so existing rotated archives stay
# readable to consumers.
- name: "Relax mode on pre-existing caddy log files"
ansible.builtin.file:
path: "{{ item.path }}"
mode: "0644"
loop: "{{ caddy_log_files.files }}"
loop_control:
label: "{{ item.path }}"
- name: "Copy caddy file"
ansible.builtin.template:
src: "./files/{{ app_name }}/Caddyfile.j2"
src: "./files/{{ app_name }}/Caddyfile.template"
dest: "{{ (caddy_file_dir, 'Caddyfile') | path_join }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
@@ -51,7 +81,7 @@
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.yml.j2"
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
+66
View File
@@ -0,0 +1,66 @@
---
- name: "Configure calibre application"
hosts: all
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "calibre"
app_user: "{{ app_name }}"
app_owner_uid: 1102
app_owner_gid: 1102
base_dir: "{{ (application_dir, app_name) | path_join }}"
config_dir: "{{ (base_dir, 'config') | path_join }}"
books_dir: "{{ (base_dir, 'books') | path_join }}"
tasks:
- name: "Create user and environment"
ansible.builtin.import_role:
name: owner
vars:
owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories"
ansible.builtin.file:
path: "{{ item }}"
state: "directory"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ base_dir }}"
- "{{ books_dir }}"
- "{{ config_dir }}"
- name: "Create backup targets file"
ansible.builtin.lineinfile:
path: "{{ base_dir }}/backup-targets"
line: "{{ item }}"
create: true
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ books_dir }}"
- "{{ config_dir }}"
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
- name: "Run application with docker compose"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "present"
remove_orphans: true
tags:
- run-app
+1
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
tasks:
# - name: "Install python docker lib from pip"
+1
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "dozzle"
+21 -5
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
# See: https://github.com/zyedidia/eget/releases
@@ -23,7 +24,7 @@
ansible.builtin.command:
cmd: >
{{ eget_bin_path }} rclone/rclone --quiet --upgrade-only --to {{ eget_install_dir }} --asset zip
--tag v1.72.0
--tag v1.73.4
changed_when: false
- name: "Install restic"
@@ -33,11 +34,19 @@
--tag v0.18.1
changed_when: false
- name: "Install resticprofile"
ansible.builtin.command:
cmd: >
{{ eget_bin_path }} creativeprojects/resticprofile --quiet --upgrade-only --to {{ eget_install_dir }}
--asset '^no_self_update'
--tag v0.32.0
changed_when: false
- name: "Install btop"
ansible.builtin.command:
cmd: >
{{ eget_bin_path }} aristocratos/btop --quiet --upgrade-only --to {{ eget_install_dir }}
--tag v1.4.5
--tag v1.4.6
changed_when: false
- name: "Install gobackup"
@@ -51,12 +60,19 @@
ansible.builtin.command:
cmd: >
{{ eget_bin_path }} go-task/task --quiet --upgrade-only --to {{ eget_install_dir }} --asset tar.gz
--tag v3.45.5
--tag v3.48.0
changed_when: false
- name: 'Install dust'
ansible.builtin.command:
cmd: >
{{ bin_prefix }}/eget bootandy/dust --quiet --upgrade-only --to {{ bin_prefix }} --asset gnu
--tag v1.2.3
{{ eget_bin_path }} bootandy/dust --quiet --upgrade-only --to {{ bin_prefix }} --asset gnu
--tag v1.2.4
changed_when: false
- name: 'Install zellij'
ansible.builtin.command:
cmd: >
{{ eget_bin_path }} zellij-org/zellij --quiet --upgrade-only --to {{ bin_prefix }} --asset no-web
--tag v0.43.1
changed_when: false
+3 -2
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "gitea"
@@ -38,7 +39,7 @@
- name: "Copy backup script"
ansible.builtin.template:
src: "files/{{ app_name }}/backup.sh.j2"
src: "files/{{ app_name }}/backup.template.sh"
dest: "{{ base_dir }}/backup.sh"
owner: "{{ app_user }}"
group: "{{ app_user }}"
@@ -46,7 +47,7 @@
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.yml.j2"
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
+90
View File
@@ -0,0 +1,90 @@
---
- name: "Configure goaccess application"
hosts: all
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "goaccess"
app_user: "{{ app_name }}"
app_owner_uid: 1106
app_owner_gid: 1106
base_dir: "{{ (application_dir, app_name) | path_join }}"
db_dir: "{{ (base_dir, 'db') | path_join }}"
report_dir: "{{ (base_dir, 'report') | path_join }}"
tasks:
- name: "Create user and environment"
ansible.builtin.import_role:
name: owner
vars:
owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create internal application directories"
ansible.builtin.file:
path: "{{ item }}"
state: "directory"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0770"
loop:
- "{{ base_dir }}"
- "{{ db_dir }}"
- "{{ report_dir }}"
# Earlier runs left root-owned files inside db/report (the
# containers used to start as root). Recurse-chown realigns them
# so the now-non-root processor can rewrite/restore them.
- name: "Realign ownership of generated artefacts"
ansible.builtin.file:
path: "{{ item }}"
state: "directory"
owner: "{{ app_user }}"
group: "{{ app_user }}"
recurse: true
loop:
- "{{ db_dir }}"
- "{{ report_dir }}"
- name: "Ensure caddy access log exists before goaccess starts"
ansible.builtin.copy:
content: ""
dest: "{{ (caddy_logs_dir, 'access.log') | path_join }}"
force: false
owner: "root"
group: "root"
mode: "0644"
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
- name: "Copy Dockerfile and entrypoint for the local jq-enabled goaccess image"
ansible.builtin.copy:
src: "./files/{{ app_name }}/{{ item.name }}"
dest: "{{ (base_dir, item.name) | path_join }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "{{ item.mode }}"
loop:
- {name: "Dockerfile", mode: "0640"}
- {name: "entrypoint.sh", mode: "0750"}
- name: "Run application with docker compose"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "present"
build: "always"
remove_orphans: true
tags:
- run-app
+1
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "gramps"
+1 -1
View File
@@ -5,8 +5,8 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
- vars/homepage.yml
- vars/homepage.images.yml
tasks:
+7 -4
View File
@@ -4,8 +4,8 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
- vars/homepage.yml
- vars/homepage.images.yml
tasks:
- name: "Create user and environment"
@@ -27,9 +27,11 @@
loop:
- "{{ base_dir }}"
- name: "Login to yandex docker registry."
ansible.builtin.script:
cmd: "files/yandex-docker-registry-auth.sh"
- name: "Login to Yandex Container Registry"
community.docker.docker_login:
registry_url: "{{ yc_container_registry }}"
username: "oauth"
password: "{{ yc_oauth_token }}"
- name: "Copy docker compose file"
ansible.builtin.template:
@@ -44,5 +46,6 @@
project_src: "{{ base_dir }}"
state: "present"
remove_orphans: true
pull: "always"
tags:
- run-app
+4 -2
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "memos"
@@ -39,7 +40,7 @@
- name: "Copy gobackup config"
ansible.builtin.template:
src: "./files/{{ app_name }}/gobackup.yml.j2"
src: "./files/{{ app_name }}/gobackup.template.yml"
dest: "{{ gobackup_config }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
@@ -47,7 +48,7 @@
- name: "Copy backup script"
ansible.builtin.template:
src: "files/{{ app_name }}/backup.sh.j2"
src: "files/{{ app_name }}/backup.template.sh"
dest: "{{ base_dir }}/backup.sh"
owner: "{{ app_user }}"
group: "{{ app_user }}"
@@ -72,6 +73,7 @@
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
register: docker_compose_file_result
- name: "Run application with docker compose"
community.docker.docker_compose_v2:
+1
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "miniflux"
+40
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "netdata"
@@ -13,6 +14,7 @@
base_dir: "{{ (application_dir, app_name) | path_join }}"
config_dir: "{{ (base_dir, 'config') | path_join }}"
config_go_d_dir: "{{ (config_dir, 'go.d') | path_join }}"
config_health_d_dir: "{{ (config_dir, 'health.d') | path_join }}"
data_dir: "{{ (base_dir, 'data') | path_join }}"
tasks:
@@ -37,6 +39,7 @@
- "{{ data_dir }}"
- "{{ config_dir }}"
- "{{ config_go_d_dir }}"
- "{{ config_health_d_dir }}"
- name: "Copy netdata config file"
ansible.builtin.template:
@@ -75,6 +78,43 @@
loop: "{{ go_d_existing_files.files }}"
when: (item.path | basename) not in (go_d_source_files.files | map(attribute='path') | map('basename') | list)
- name: "Find all health.d config files"
ansible.builtin.find:
paths: "files/{{ app_name }}/health.d"
file_type: file
delegate_to: localhost
register: health_d_source_files
- name: "Template all health.d config files"
ansible.builtin.template:
src: "{{ item.path }}"
dest: "{{ config_health_d_dir }}/{{ item.path | basename }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
loop: "{{ health_d_source_files.files }}"
- name: "Find existing health.d config files on server"
ansible.builtin.find:
paths: "{{ config_health_d_dir }}"
file_type: file
register: health_d_existing_files
- name: "Remove health.d config files that don't exist in source"
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ health_d_existing_files.files }}"
when: (item.path | basename) not in (health_d_source_files.files | map(attribute='path') | map('basename') | list)
- name: "Copy health alarm notify config"
ansible.builtin.template:
src: "files/{{ app_name }}/health_alarm_notify.template.conf"
dest: "{{ config_dir }}/health_alarm_notify.conf"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
- name: "Grab docker group id."
ansible.builtin.shell:
cmd: |
+30 -10
View File
@@ -4,16 +4,22 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "outline"
app_user: "{{ app_name }}"
app_owner_uid: 1007
app_owner_gid: 1008
base_dir: "{{ (application_dir, app_name) | path_join }}"
data_dir: "{{ (base_dir, 'data') | path_join }}"
postgres_data_dir: "{{ (base_dir, 'data', 'postgres') | path_join }}"
postgres_backups_dir: "{{ (base_dir, 'backups', 'postgres') | path_join }}"
media_dir: "{{ (base_dir, 'media') | path_join }}"
backups_dir: "{{ (base_dir, 'backups') | path_join }}"
postgres_data_dir: "{{ (data_dir, 'postgres') | path_join }}"
postgres_backups_dir: "{{ (backups_dir, 'postgres') | path_join }}"
tasks:
- name: "Create user and environment"
@@ -35,17 +41,11 @@
loop:
- "{{ base_dir }}"
- "{{ data_dir }}"
- "{{ media_dir }}"
- "{{ backups_dir }}"
- "{{ postgres_data_dir }}"
- "{{ postgres_backups_dir }}"
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
- name: "Copy backup script"
ansible.builtin.template:
src: "./files/{{ app_name }}/backup.template.sh"
@@ -54,6 +54,26 @@
group: "{{ app_user }}"
mode: "0750"
- name: "Create backup targets file"
ansible.builtin.lineinfile:
path: "{{ base_dir }}/backup-targets"
line: "{{ item }}"
create: true
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ media_dir }}"
- "{{ backups_dir }}"
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
- name: "Run application with docker compose"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
+81
View File
@@ -0,0 +1,81 @@
---
- name: "Configure remembos application"
hosts: all
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "remembos"
app_user: "{{ app_name }}"
app_owner_uid: 1103
app_owner_gid: 1103
base_dir: "{{ (application_dir, app_name) | path_join }}"
config_dir: "{{ (base_dir, 'config') | path_join }}"
data_dir: "{{ (base_dir, 'data') | path_join }}"
config_file: "{{ (config_dir, 'config.toml') | path_join }}"
tasks:
- name: "Create user and environment"
ansible.builtin.import_role:
name: owner
vars:
owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories"
ansible.builtin.file:
path: "{{ item }}"
state: "directory"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ base_dir }}"
- "{{ data_dir }}"
- "{{ config_dir }}"
- name: "Copy config"
ansible.builtin.template:
src: "./files/{{ app_name }}/config.template.toml"
dest: "{{ config_file }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
register: config_file_result
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
register: docker_compose_file_result
- name: 'Login to Yandex Container Registry'
community.docker.docker_login:
registry_url: '{{ yc_container_registry }}'
username: 'oauth'
password: '{{ yc_oauth_token }}'
- name: "Run application with docker compose"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "present"
remove_orphans: true
tags:
- run-app
- name: "Restart docker compose services if config changed but not docker-compose.yml"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "restarted"
when:
- config_file_result.changed
- not docker_compose_file_result.changed
tags:
- run-app
+6
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
user_name: "<put-name-here>"
@@ -25,6 +26,11 @@
path: "/var/www/{{ user_name }}"
state: absent
- name: "Remove application dir"
ansible.builtin.file:
path: "{{ (application_dir, user_name) | path_join }}"
state: absent
- name: "Remove home dir"
ansible.builtin.file:
path: "/home/{{ user_name }}"
+2 -1
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "rssbridge"
@@ -34,7 +35,7 @@
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.yml.j2"
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
+5 -3
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
apt_packages:
@@ -40,9 +41,9 @@
group: root
mode: "0755"
- name: 'Create directory for mount'
- name: 'Create directory for applications'
ansible.builtin.file:
path: '/mnt/applications'
path: '{{ application_dir }}'
state: 'directory'
mode: '0755'
tags:
@@ -50,9 +51,10 @@
- name: 'Mount external storages'
ansible.posix.mount:
path: '/mnt/applications'
path: '{{ application_dir }}'
src: 'UUID=3942bffd-8328-4536-8e88-07926fb17d17'
fstype: ext4
state: mounted
when: mount_external_storage | default(false) | bool
tags:
- mount-storage
+1
View File
@@ -5,6 +5,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
- vars/transcriber.yml
- vars/transcriber.images.yml
+6 -3
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
- vars/transcriber.yml
- vars/transcriber.images.yml
@@ -38,9 +39,11 @@
group: "{{ app_user }}"
mode: "0600"
- name: "Login to yandex docker registry."
ansible.builtin.script:
cmd: "files/yandex-docker-registry-auth.sh"
- name: "Login to Yandex Container Registry"
community.docker.docker_login:
registry_url: "{{ yc_container_registry }}"
username: "oauth"
password: "{{ yc_oauth_token }}"
- name: "Copy docker compose file"
ansible.builtin.template:
+74
View File
@@ -0,0 +1,74 @@
---
- name: "Configure tuwunel matrix server"
hosts: all
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "tuwunel"
app_user: "{{ app_name }}"
app_owner_uid: 1105
app_owner_gid: 1105
base_dir: "{{ (application_dir, app_name) | path_join }}"
data_dir: "{{ (base_dir, 'data') | path_join }}"
backups_dir: "{{ (base_dir, 'backups') | path_join }}"
tuwunel_server_name: "vakhrushev.me"
tuwunel_well_known_server: "matrix.vakhrushev.me:443"
tuwunel_well_known_client: "https://matrix.vakhrushev.me"
tasks:
- name: "Create user and environment"
ansible.builtin.import_role:
name: owner
vars:
owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories"
ansible.builtin.file:
path: "{{ item }}"
state: "directory"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ base_dir }}"
- "{{ data_dir }}"
- "{{ backups_dir }}"
- name: "Disable backup script"
ansible.builtin.file:
dest: "{{ base_dir }}/backup.sh"
state: absent
- name: "Create backup targets file"
ansible.builtin.lineinfile:
path: "{{ base_dir }}/backup-targets"
line: "{{ item }}"
create: true
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ data_dir }}"
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
- name: "Run application with docker compose"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "present"
remove_orphans: true
tags:
- run-app
+59
View File
@@ -0,0 +1,59 @@
---
- name: "Configure UFW firewall"
hosts: all
vars_files:
- vars/secrets.yml
- vars/vars.yml
tasks:
- name: "Ensure UFW is installed"
ansible.builtin.apt:
name: ufw
state: present
update_cache: true
- name: "Set default incoming policy to deny"
community.general.ufw:
direction: incoming
policy: deny
- name: "Set default outgoing policy to allow"
community.general.ufw:
direction: outgoing
policy: allow
- name: "Allow SSH on port 22"
community.general.ufw:
rule: allow
port: "22"
proto: tcp
- name: "Allow Gitea SSH on port 2222"
community.general.ufw:
rule: allow
port: "2222"
proto: tcp
- name: "Allow HTTP on port 80/tcp"
community.general.ufw:
rule: allow
port: "80"
proto: tcp
- name: "Allow HTTPS on port 443/tcp"
community.general.ufw:
rule: allow
port: "443"
proto: tcp
- name: "Allow HTTPS QUIC on port 443/udp"
community.general.ufw:
rule: allow
port: "443"
proto: udp
- name: "Enable UFW"
community.general.ufw:
state: enabled
logging: true
+1
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
tasks:
- name: Perform an upgrade of packages
+4 -3
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "wakapi"
@@ -39,7 +40,7 @@
- name: "Copy gobackup config"
ansible.builtin.template:
src: "./files/{{ app_name }}/gobackup.yml.j2"
src: "./files/{{ app_name }}/gobackup.template.yml"
dest: "{{ gobackup_config }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
@@ -47,7 +48,7 @@
- name: "Copy backup script"
ansible.builtin.template:
src: "files/{{ app_name }}/backup.sh.j2"
src: "files/{{ app_name }}/backup.template.sh"
dest: "{{ base_dir }}/backup.sh"
owner: "{{ app_user }}"
group: "{{ app_user }}"
@@ -55,7 +56,7 @@
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.yml.j2"
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
+1
View File
@@ -4,6 +4,7 @@
vars_files:
- vars/secrets.yml
- vars/vars.yml
vars:
app_name: "wanderer"
+2
View File
@@ -5,3 +5,5 @@ ungrouped:
ansible_host: "158.160.46.255"
ansible_user: "major"
ansible_become: true
application_dir: "/mnt/applications"
mount_external_storage: true
+16
View File
@@ -0,0 +1,16 @@
[project]
name = "pet-project-server"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"ansible>=13.2.0",
"ansible-lint>=25.12.2",
"invoke>=2.2.1",
"mypy>=1.19.1",
"requests>=2.32.5",
"ruff>=0.15.2",
"types-requests>=2.32.4.20260107",
"yamllint>=1.37.1",
]
+11 -6
View File
@@ -1,9 +1,14 @@
---
- src: yatesr.timezone
version: 1.2.2
roles:
- src: 'yatesr.timezone'
version: '1.2.2'
- src: geerlingguy.security
version: 3.0.0
- src: 'geerlingguy.security'
version: '3.0.0'
- src: geerlingguy.docker
version: 7.4.7
- src: 'geerlingguy.docker'
version: '7.9.0'
collections:
- name: 'community.docker'
- name: 'community.general'
+5 -4
View File
@@ -22,7 +22,7 @@
groups: "{{ owner_extra_groups }}"
uid: "{{ owner_uid }}"
shell: /bin/bash
register: user_create_result
register: owner_create_result
- name: 'Set up user ssh keys for user "{{ owner_name }}".'
ansible.posix.authorized_key:
@@ -34,11 +34,12 @@
- name: "Prepare env variables."
ansible.builtin.set_fact:
env_dict: '{{ owner_env | combine({"USER_UID": user_create_result.uid, "USER_GID": user_create_result.group}) }}'
# yamllint disable-line rule:line-length
owner_env_dict: '{{ owner_env | combine({"USER_UID": owner_create_result.uid, "USER_GID": owner_create_result.group}) }}'
- name: 'Set up environment variables for user "{{ owner_name }}".'
ansible.builtin.template:
src: env.j2
src: env.template
dest: "/home/{{ owner_name }}/.env"
owner: "{{ owner_name }}"
group: "{{ owner_group }}"
@@ -49,7 +50,7 @@
path: "/home/{{ owner_name }}/.bashrc"
regexp: "^export {{ item.key }}="
state: absent
with_dict: "{{ env_dict }}"
with_dict: "{{ owner_env_dict }}"
- name: 'Include in bashrc environment variables for user "{{ owner_name }}".'
ansible.builtin.lineinfile:
+3
View File
@@ -0,0 +1,3 @@
{% for name in owner_env_dict.keys() | sort %}
{{ name }}={{ owner_env_dict[name] }}
{% endfor %}
@@ -39,11 +39,11 @@ TERMINAL = "aws4_request"
VERSION = 0x04
def sign(key, msg):
def sign(key: bytes, msg: str) -> bytes:
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
def calculate_key(secret_access_key):
def calculate_key(secret_access_key: str) -> str:
signature = sign(("AWS4" + secret_access_key).encode("utf-8"), DATE)
signature = sign(signature, REGION)
signature = sign(signature, SERVICE)
@@ -54,7 +54,7 @@ def calculate_key(secret_access_key):
return smtp_password.decode("utf-8")
def main():
def main() -> None:
if sys.version_info[0] < 3:
raise Exception("Must be using Python 3")
+3 -3
View File
@@ -16,11 +16,11 @@ import sys
from email.message import EmailMessage
def send_test_email(login, password, to_email):
def send_test_email(login: str, password: str, to_email: str) -> None:
"""Отправляет тестовое email через Yandex Cloud Postbox SMTP"""
# initialize connection to our email server
smtp = smtplib.SMTP("postbox.cloud.yandex.net", port="587")
smtp = smtplib.SMTP("postbox.cloud.yandex.net", port=587)
smtp.set_debuglevel(2)
@@ -43,7 +43,7 @@ def send_test_email(login, password, to_email):
print(f"Test email successfully sent to {to_email}")
def main():
def main() -> None:
parser = argparse.ArgumentParser(
description="Send a test email via Yandex Cloud Postbox SMTP server."
)
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Invoke tasks — замена Taskfile.yml"""
import os
import subprocess
import sys
from invoke.context import Context
from invoke.exceptions import Exit
from invoke.tasks import task
HOSTS_FILE = "production.yml"
VARS_FILE = "vars/vars.yml"
AUTHELIA_DOCKER = "docker run --rm -v $PWD:/data authelia/authelia:4.39.4 authelia"
def _yq(query: str) -> str:
result = subprocess.run(
["yq", query, HOSTS_FILE], capture_output=True, text=True, check=True
)
return result.stdout.strip()
def _remote_user() -> str:
return _yq(".ungrouped.hosts.server.ansible_user")
def _remote_host() -> str:
return _yq(".ungrouped.hosts.server.ansible_host")
def _application_dir() -> str:
"""Чтение application_dir: сначала из inventory (override), затем из vars/vars.yml."""
inv_value = _yq('.ungrouped.hosts.server.application_dir // ""')
if inv_value:
return inv_value
result = subprocess.run(
["yq", ".application_dir", VARS_FILE],
capture_output=True,
text=True,
check=True,
)
value = result.stdout.strip()
if not value or value == "null":
raise Exit(
f"application_dir не определён ни в inventory, ни в {VARS_FILE}", code=1
)
return value
def _rest_args() -> list[str]:
"""Возвращает аргументы после '--' из sys.argv"""
try:
return sys.argv[sys.argv.index("--") + 1 :]
except ValueError:
return []
def _resolve_playbook(name: str) -> str:
candidates = [name, f"{name}.yml", f"playbook-{name}.yml"]
for candidate in candidates:
if os.path.isfile(candidate):
return candidate
raise Exit(
f"Плейбук для '{name}' не найден. Проверял: {', '.join(candidates)}", code=1
)
@task
def install_roles(ctx: Context) -> None:
"""Установить ansible-galaxy roles"""
ctx.run("uv run ansible-galaxy role install --role-file requirements.yml --force")
@task
def pl(ctx: Context) -> None:
"""Запустить плейбуки по имени: inv pl -- gitea miniflux"""
names = _rest_args()
if not names:
raise Exit("Укажи хотя бы один плейбук: inv pl -- <name> [name ...]", code=1)
playbooks = [_resolve_playbook(name) for name in names]
ctx.run(
f"uv run ansible-playbook -i {HOSTS_FILE} --diff {' '.join(playbooks)}",
pty=True,
)
@task
def ssh(ctx: Context) -> None:
"""SSH на удалённый сервер"""
subprocess.run(f"ssh {_remote_user()}@{_remote_host()}", shell=True)
@task
def zj(ctx: Context) -> None:
"""Запуск zellij на удаленном сервере"""
subprocess.run(
f"ssh {_remote_user()}@{_remote_host()} -t zellij attach --create main",
shell=True,
)
@task(aliases=["login"])
def login_as_app(ctx: Context, app: str) -> None:
"""SSH и переключиться на пользователя приложения: inv login gitea"""
# sudo -i: login shell, -u: от имени пользователя
# bash -i: интерактивный режим (job control), -l: login (читает профиль)
app_dir = f"{_application_dir()}/{app}"
subprocess.run(
f"""ssh {_remote_user()}@{_remote_host()} -t 'sudo -iu {app} bash -c "cd {app_dir} && exec bash -il"'""",
shell=True,
)
@task
def btop(ctx: Context) -> None:
"""Запустить btop на удалённом сервере"""
subprocess.run(f"ssh {_remote_user()}@{_remote_host()} -t btop", shell=True)
@task
def encrypt(ctx: Context, file: str) -> None:
"""Зашифровать файлы через ansible-vault"""
ctx.run(f"uv run ansible-vault encrypt {file}")
@task
def decrypt(ctx: Context, file: str) -> None:
"""Расшифровать файлы через ansible-vault"""
ctx.run(f"uv run ansible-vault decrypt {file}")
@task
def authelia_cli(ctx: Context, args: str = "") -> None:
"""Запустить authelia CLI в docker"""
ctx.run(f"{AUTHELIA_DOCKER} {args}")
@task
def authelia_validate_config(ctx: Context) -> None:
"""Отрендерить конфиг authelia из шаблона и проверить его"""
dest = "temp/configuration.yml"
try:
ctx.run(
"uv run ansible localhost"
" --module-name template"
f' --args "src=files/authelia/configuration.template.yml dest={dest}"'
" --extra-vars @vars/secrets.yml"
" --extra-vars @files/authelia/secrets.yml"
)
ctx.run(f"{AUTHELIA_DOCKER} validate-config --config /data/{dest}")
finally:
ctx.run(f"rm -f {dest}", warn=True)
@task
def authelia_gen_random_string(ctx: Context, length: int = 10) -> None:
"""Сгенерировать случайную alphanumeric-строку"""
ctx.run(f"{AUTHELIA_DOCKER} crypto rand --length {length} --charset alphanumeric")
@task
def authelia_gen_secret_and_hash(ctx: Context, length: int = 72) -> None:
"""Сгенерировать случайный секрет и его pbkdf2-sha512 хэш"""
ctx.run(
f"{AUTHELIA_DOCKER} crypto hash generate pbkdf2"
f" --variant sha512 --random --random.length {length} --random.charset rfc3986"
)
@task
def format_py_files(ctx: Context) -> None:
"""Отформатировать Python-файлы через Black в docker"""
uid = os.getuid()
gid = os.getgid()
ctx.run(
f"docker run --rm -u {uid}:{gid} -v $PWD:/app -w /app"
" pyfound/black:latest_release black ."
)
-3
View File
@@ -1,3 +0,0 @@
{% for name in env_dict.keys() | sort %}
{{ name }}={{ env_dict[name] }}
{% endfor %}
Generated
+951
View File
@@ -0,0 +1,951 @@
version = 1
revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.14'",
"python_full_version < '3.14'",
]
[[package]]
name = "ansible"
version = "13.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ansible-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/18/f13c9a462ef20893d30bd11c4089edee02b5ef1d31e1c2634da592732279/ansible-13.2.0.tar.gz", hash = "sha256:fac46e202d1020027341659918b39e588dd7c43cef26537d7ca7fe51c324fe31", size = 52000144, upload-time = "2025-12-30T16:42:37.456Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/dc/1b0d0c7b83a75e574b3b56d179f887396d135aad5ab15cc45e121bc4d307/ansible-13.2.0-py3-none-any.whl", hash = "sha256:8a7f536542d4b18118200b8eaba40aa62ed990bd0e7b7622368b656b67db056f", size = 54530175, upload-time = "2025-12-30T16:42:32.195Z" },
]
[[package]]
name = "ansible-compat"
version = "25.12.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/75/62/fb3aee58827ba6eacee561183b02b4a49cf4819f405df2409e23bad69a00/ansible_compat-25.12.0.tar.gz", hash = "sha256:d38a149c5f95bb0d529c3b5e5fa3200b26f5e938ec7c31d8bb01d87c1f634410", size = 193698, upload-time = "2025-12-02T15:36:37.831Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/cb/be446a7db8b7ade13ebea507a7075a1eb8fd900c068c12ceee537a06ae9f/ansible_compat-25.12.0-py3-none-any.whl", hash = "sha256:322ccc7002030a91588d1fdba21e934703d64736f0e483dc6afe8ecd93a75614", size = 27731, upload-time = "2025-12-02T15:36:36.072Z" },
]
[[package]]
name = "ansible-core"
version = "2.20.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/3e/47/3543ea4e7ad65859c0043e9a03e1da99c57c22dfb29027e9951dd58e7524/ansible_core-2.20.1.tar.gz", hash = "sha256:a891e5f90cd46626778f0f3d545ec1115840c9b50e8adf25944c5e1748452106", size = 3313203, upload-time = "2025-12-09T16:49:57.189Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/8c/b9ef852c9322bffd08ef72c8d6737922af810920e8ebd6ae5c9c5ac1f623/ansible_core-2.20.1-py3-none-any.whl", hash = "sha256:2a66825b4a53f130b62515e7e2a3d811d544b19b6e8a22d9ef88c55896384cb3", size = 2412350, upload-time = "2025-12-09T16:49:55.562Z" },
]
[[package]]
name = "ansible-lint"
version = "25.12.2"
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/0a/b1/cbbe86f424d76bb621461d131506e5778556ddf6f5eba0cf7c41db829208/ansible_lint-25.12.2.tar.gz", hash = "sha256:d4dbf2905c868d6f7b330941efc586f19a5e2b3b6826fd6a2edf0d5080329e46", size = 725323, upload-time = "2025-12-22T17:01:20.433Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/ea/d5620576753c7424a41b9d97ea050297ec2d7cc1e4fa12e44c27289726a2/ansible_lint-25.12.2-py3-none-any.whl", hash = "sha256:cafcd991c9ddfd9a13e5b56e5a4f1d9997973e0c0d6a0f6eb2b148d39f42e2c3", size = 323017, upload-time = "2025-12-22T17:01:18.698Z" },
]
[[package]]
name = "attrs"
version = "25.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
]
[[package]]
name = "black"
version = "25.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
{ name = "pytokens" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" },
{ url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" },
{ url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" },
{ url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" },
{ url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" },
{ url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" },
{ url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" },
{ url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" },
{ url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" },
{ url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" },
{ url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" },
{ url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" },
{ url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" },
{ url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" },
{ url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" },
]
[[package]]
name = "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.2.25"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" },
{ url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" },
{ url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" },
{ url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" },
{ url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" },
{ url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" },
{ url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" },
{ url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" },
{ url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" },
{ url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" },
{ url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" },
{ url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" },
{ url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" },
{ url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" },
{ url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" },
{ url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" },
{ url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" },
{ url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" },
{ url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" },
{ url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" },
{ url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" },
{ url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" },
{ url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" },
{ url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" },
{ url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" },
{ url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" },
{ url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" },
{ url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" },
{ url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" },
{ url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" },
{ url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" },
{ url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" },
{ url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" },
{ url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" },
{ url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" },
{ url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" },
{ url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" },
{ url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" },
{ url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" },
{ url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" },
{ url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" },
{ url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" },
{ url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" },
{ url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" },
{ url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" },
{ url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" },
{ url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" },
{ url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" },
]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cryptography"
version = "46.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
]
[[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 = "filelock"
version = "3.20.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c1/e0/a75dbe4bca1e7d41307323dad5ea2efdd95408f74ab2de8bd7dba9b51a1a/filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64", size = 19510, upload-time = "2026-01-02T15:33:32.582Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8", size = 16697, upload-time = "2026-01-02T15:33:31.133Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "invoke"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" },
]
[[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.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "jsonschema-specifications" },
{ name = "referencing" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
]
[[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 = "librt"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" },
{ url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" },
{ url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" },
{ url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" },
{ url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" },
{ url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" },
{ url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" },
{ url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" },
{ url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" },
{ url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" },
{ url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" },
{ url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" },
{ url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" },
{ url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" },
{ url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" },
{ url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" },
{ url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" },
{ url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" },
{ url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" },
{ url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" },
{ url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" },
{ url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" },
{ url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" },
{ url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" },
{ url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" },
{ url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" },
{ url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" },
{ url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" },
{ url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" },
{ url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" },
{ url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" },
{ url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" },
{ url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" },
{ url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" },
{ url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" },
{ url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" },
{ url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" },
{ url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" },
{ url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" },
{ url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" },
{ url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" },
{ url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" },
{ url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" },
{ url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" },
{ url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" },
{ url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" },
{ url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" },
{ url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" },
{ url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" },
]
[[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 = "mypy"
version = "1.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
{ url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
{ url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
{ url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
{ url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
{ url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
{ url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
{ url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
{ url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
{ url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
{ url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
{ url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
{ url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
{ url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
{ url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "pet-project-server"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "ansible" },
{ name = "ansible-lint" },
{ name = "invoke" },
{ name = "mypy" },
{ name = "requests" },
{ name = "ruff" },
{ name = "types-requests" },
{ name = "yamllint" },
]
[package.metadata]
requires-dist = [
{ name = "ansible", specifier = ">=13.2.0" },
{ name = "ansible-lint", specifier = ">=25.12.2" },
{ name = "invoke", specifier = ">=2.2.1" },
{ name = "mypy", specifier = ">=1.19.1" },
{ name = "requests", specifier = ">=2.32.5" },
{ name = "ruff", specifier = ">=0.15.2" },
{ name = "types-requests", specifier = ">=2.32.4.20260107" },
{ name = "yamllint", specifier = ">=1.37.1" },
]
[[package]]
name = "platformdirs"
version = "4.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
]
[[package]]
name = "pycparser"
version = "2.23"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "pytokens"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "referencing"
version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "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 = "rpds-py"
version = "0.30.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
{ url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
{ url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
{ url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
{ url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
{ url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
{ url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
{ url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
{ url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
{ url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
{ url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
{ url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
{ url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
{ url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
{ url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
{ url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
{ url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
{ url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
{ url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
{ url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
{ url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
{ url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
{ url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
{ url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
{ url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
{ url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
{ url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
{ url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
{ url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
{ url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
{ url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
{ url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
{ url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
{ url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
{ url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
{ url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
{ url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
{ url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
{ url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
{ url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
{ url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
{ url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
{ url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
{ url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
{ url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
{ url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
{ url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
{ url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
{ url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
{ url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
{ url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
{ url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
{ url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
{ url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
{ url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
{ url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
{ url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
{ url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
{ url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
{ url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
{ url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
{ url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
{ url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
{ url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
{ url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
{ url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
]
[[package]]
name = "ruamel-yaml"
version = "0.19.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ruamel-yaml-clibz", marker = "platform_python_implementation == 'CPython'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0c/5d/8a1de57b5a11245c61c906d422cd1e66b6778e134a1c68823a451be5759c/ruamel_yaml-0.19.0.tar.gz", hash = "sha256:ff19233e1eb3e9301e7a3d437847713e361a80faace167639327efbe8c0e5f95", size = 142095, upload-time = "2025-12-31T16:47:31.837Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/3e/835d495068a4bb03419ce8c5464734ff6f3343df948e033cb5e5f81f7f08/ruamel_yaml-0.19.0-py3-none-any.whl", hash = "sha256:96ea8bafd9f3fdb0181ce3cc05e6ec02ce0a8788cbafa9b5a6e47c76fe26dfc6", size = 117777, upload-time = "2025-12-31T16:47:29.07Z" },
]
[[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 = "ruamel-yaml-clibz"
version = "0.3.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/73/2a9857a017f8ee260c683d8155634f0e3ca57964da2c3055b02822d582cd/ruamel_yaml_clibz-0.3.7.tar.gz", hash = "sha256:2be85214793cfc787eb12380fa3226e10cd7727b24551c3067c173d66c9bedd9", size = 231233, upload-time = "2026-01-02T14:52:41.734Z" }
[[package]]
name = "ruff"
version = "0.15.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
{ url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
{ url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
{ url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
{ url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
{ url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
{ url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
{ url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
{ url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
{ url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
{ url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
{ url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
{ url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
{ url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
]
[[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 = "types-requests"
version = "2.32.4.20260107"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "urllib3"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
[[package]]
name = "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.37.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pathspec" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/f2/cd8b7584a48ee83f0bc94f8a32fea38734cefcdc6f7324c4d3bfc699457b/yamllint-1.37.1.tar.gz", hash = "sha256:81f7c0c5559becc8049470d86046b36e96113637bcbe4753ecef06977c00245d", size = 141613, upload-time = "2025-05-04T08:25:54.355Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/b9/be7a4cfdf47e03785f657f94daea8123e838d817be76c684298305bd789f/yamllint-1.37.1-py3-none-any.whl", hash = "sha256:364f0d79e81409f591e323725e6a9f4504c8699ddf2d7263d8d2b539cd66a583", size = 68813, upload-time = "2025-05-04T08:25:52.552Z" },
]
-2
View File
@@ -1,2 +0,0 @@
---
homepage_nginx_image: "homepage-nginx:f797e17-1761204003"
+2
View File
@@ -6,5 +6,7 @@ app_owner_gid: 1009
base_dir: "{{ (application_dir, app_name) | path_join }}"
docker_registry_prefix: "cr.yandex/crplfk0168i4o8kd7ade"
homepage_nginx_image: "homepage-nginx:latest"
# Registry images
registry_homepage_nginx_image: "{{ (docker_registry_prefix, homepage_nginx_image) | path_join }}"
+162 -161
View File
@@ -1,162 +1,163 @@
$ANSIBLE_VAULT;1.1;AES256
66333736306664316131646233393639356436323832386638353237393761386631303639396432
6266616434663638306637356435393564303633613332640a663366336135373061396239653065
32343339313734346261363461633735383538613033656138383835653661633334316533613738
3730656661393466370a653863353234323739346630323534333334306432646636383664313865
39666133623032393638633239653665376336626638303334626164376663626666393439346661
32363364363066653933313135666566346138353163333639353062336331623938353131646334
32326466323030643364356530653862633332363338333633393730663536353363663165303838
66643331366231613733316139333233393636626162643861346437613062643133353035353566
38323062653135393364376265346361613163346337636634383932666137636133363864643264
39646634363037383662376166633335636365303734646465623833313766656235333835396263
63326237306337396465343065313263663466613336303934373337623364613839323931623934
35353665363339646365353139643032366164653133616562303930306264343633343439373830
66356635343363363361313633353134313837653163346537656465623832633561323833663763
32323537303931343934376165663466356466333533643237653038373866626439356334663539
36613336326633306331323763393230653236306337353139353135316566336661623464316237
37363930666539373837336466333265333866633733363032323862333239643764333766366134
33396330613463633261626334393262343031663131313737623233323535303965333864366631
65333535343062393434396361313433386463613630353765663363633431393831316233313162
39656333306265386437663863343638323262663734623830336562636366333534373735313932
61653739616635643661306165363364316262386434643033646430366235303334326236373262
34396261653934633337646338333364326237663739623965313039303134393238653634616431
39376539323238353033323333643337613935616335336562626361383164616432633331313536
63373633633966373237393131663064613332316536373163303163653638343264323934393365
62356636643434646465313734363936346435663237393064663138643135303237633965393965
31353564616136366237666139646561363364353738633862346262616637663362333137386133
63383662363866306366326232643462376532313632336662666666386562356436616236663430
37636661656237646431616331633833306266313434326461353561643835646236346539323337
30303065313165306233646135363766613931383433313165366662636137326162633363306431
31626564323566313036363666386338366462613164663735363333313830306238323365363737
65333137363631623139333964316464376662643661643038326162643630323938626332633130
62626334666131373731343432303461656537303062396638326134356166663936663364656139
32333864653231636235663165323936626135313838663866373132376437656236363235353533
39346532626666656233343433383434613238653833626436643566653462346330316565366564
33623831643265623966326638656462306161636366303266333734396433653861393363333332
61616463613931393835613463353039646164383435373330626134376339623861396266623430
30316365356337356531363263623362633332313536373333653964356534623861613232653932
35613064336131313632656663303631363664366163343362643365663932356138376235313466
30353333376461623435363931356665306333623736313562333836336563616137633863346635
37356137306361383134663535626130646135363661353438343961653766333538353330346236
64326639383864396336343062663965343964396162386266363639643962666431336237303864
66663861346139363335663362333032613637336266323439366566373136643133383361323061
37633134323933613665366665383962373935646137616139353661336665613661623834303465
30323264333137636261393535376438363134663734313662383533313130623365386335323662
63366139643238613632326165313835363964336237383936333737646239363365623030666364
64656432653164643565376666373262333839353139666561623731343234326637316636333765
63306331343538386433376566326239376232363434343838653864393562383063663566333263
35326333326462316134383139303534343263646530363266663933353834353138303435646339
33643361363062373735663430346636333431363736373463326439353437356530373935633962
63333031376163656565663536366230333731613833396266383465333461386161373337323863
36313032613534346230636566383930656330656133376431376462656536386263343831393862
32363365623061653837303736636664663361663862656562393661623330386435646336373531
35363037356638653831386261646235613337363066343632653632306631633138633235356139
39663936333262643061646330316538373862353030626632396336393030643239316634343730
35363564373865376533333466666439646633313932656665383930623531633038316363636332
37363466643835353132333532646163316636303662646234613038333334626365623964653235
36393739353164306666313537633538383934363330373235353262616165623132613330303735
65323362666263653937376562633833653264613439303236373466646362316462386632373038
35353462633730666430623638626439626364616335643964623933663233376433353233633235
34616233636335323365646538326639383033643832323139633064616635353331376230663738
35316665616230633335366233363332353432633937653335316662306166383337633262633634
35303766653733376362623436383663663437393461643266376530613533383038373035356234
32343962623930336566363164616361386134376362383138653963366339326431653832393664
66633566343666326339323536343738396532623735343034646130386332323466346365383135
33373732363833316138343938633262373066613930343162663163323331373466366631626237
34623062353139353734653366313363323634346564663861613233366537663732363134643638
65623964396665303765343430623636376637666364363835346462613763306135326463353631
33333834333464383437336531616662643461666138383435636530373761646564626438313433
64643037626637343433366631343266373436343864396663353039333231646462333932353436
33643837616639666363353237313137353133346636383231623634326335386537363030613537
33306333363638643533373237633833663333393566656466313832383636663031663433636663
37313034353734343966613934663530643537366562623137373331623632653466333839376331
61623866633937666365633763613138346365393934383163623730373134373531616138363733
65616230303337396561666462653866333438353463316235303331643834653165363033626537
37386636653337363666393163393031333461383331613965303262616530363133373732313362
33343033373963656333363064303035666663646536373764323833653037316231316430333634
65383236663035326636336535636462323438363165633437333337613439326433626466396365
38393362633266663962646565336539333239346133323434646632643537613435643434623631
63636536393936343165393966663438343261333966363639323462663566613437393838323036
33393163323034336533333632646230343138336333353236376136383664646466626666356234
61653466363933333331613539636431393934393235376433326665643263663638306463393837
34396339383536636461366230383938386339303334393038343239363361666565336237326465
63623231663861303436353533663661656431646165646662383065386362636633333631643335
38373464373432626538616234623638303734626237393566326463633765316365653837303433
34376438323439633237313733343836343733373930643138636333366166353666373966323231
63613466306365386137336538613837613264633735393937343166303736396162303230623430
33363233663761333063363134393935333530343133303138373934666236393732396330643438
61343231666363323834346132376135303339313766383365363837313163393636333563376436
64333832393361626663376161343033383763373537396264646239303736333263303739326436
31393665363734323364643032393031313135623563383639316163616535323938343765356264
35616433396236396662393930646431643063323163343366313030383766323733333865616235
65623737663763373363613030326665613333636366393464383139626462346333323162333537
32386635636532346539623534346364623532656435653638643962353836653330316638633661
38363330656161393766383237663737663835646565363966373563663832313263633238316165
64643536336232396436636261303866376132366261373132333631646166393764636130326335
39613037633239626666396262386533343764623233626335373139663937613433356134633330
32656138383563343734616536303265306661386362343939626332623763393136323138653937
39646537366461353364616538663361663465626634356338323166373837323339343061323261
33393765653034643166313639323962306265383336663265623832393038386334643661336663
66643330306239383736636330393266346537326436663432653031303765393930633761343363
61353163323830656538373366323230353830633534326133613861356132623938663565393130
63333764613761663162633035353036663736656138623234313833326337316438323835313465
31653136356638623965363266636461646465313065666137313931623063303337393963353437
39633166326662303834303261353965363235626535643564613064306636366461666263383833
66363966636538373435323365636630383561336533356631646563646536383333393864613164
38393638623263666239623835613933393564323132373630623734363334663735633438663137
35613563356161316632323562626639633931643762643232636136653731323337333236326363
36633338386230626665373666643831626132323866653430393531336466386463663865393962
37363633343632653963306531393537633762313565303631643064363531363839643363356139
33666162306331393237333966643735643731633331373839356637636462633836633762653039
32633630303437633130613366386633346637623266663361656365633737346432343263646536
35376433386464383533323231353830666363363437623033643238363438303565616137316461
64636233643365663666656431613966396561366236383363303135613433626466366437396465
37303562633336636635363133626238643430663132626331316437626532356237383730626665
38373633323531346366313631376231353966626637636262333936343066336639396262646436
31616165626466623066336138653966323334326236663439643561663863323130653631643231
31396335333066326166383337353033376335376664653162396466346638653531346239353936
33663862323135623432333036336162353061363537363463366435643461356562323935653962
62333130616466353032316462356233393037326438303064656235323966306535346132663232
63393238646534316433643638646536313934666361323061306561396132306431633932313031
31356430313463613331363064363265646361353430646563396238326330323038666264653935
63306635323439326665306135643730386239356661303232353737383532353363303163313437
38623064383035373239623966336563626130636335383833383366343130393939316535333466
65353062396632373330663365373761656161653837396666376531376663313466393230646461
62306465363166663962316634663763626666306631363731633834366433363630383833393361
36393932326138643432666437373463343061313135613163343034373464356666333134366664
66343461393733343130636266623661323066623332633930326361626436366237323664336637
61316135613330663835353139613039613264383838313337373432623133363735333732376566
38633763336438663737653736326466646237313130623934396531306430363461653962336132
64313437323766613632376439336234393165646338396334373037323237633737393866303231
33646338333532613432393234316235653466633639306465363062656335353034666665623631
66386437616462633336636566393537323831313566326637386466616331396464653438396562
33343132626463393364386133323163393336313065316433613133663961633033613232343337
37313334386533366465353461633930643662326235366139306335656163313864636161623239
38336238663630313836363739623334633130616433393536303431333735643565326265613561
30613762396162633964336165666137626333643735323330646266666563313935623230643262
30383635663035653437333339303730346366333765353739663231643433353764363966353435
63663466343864363033646663376261363562636630643038613365333936653165633733623134
36626135313330303463336535663235323536613661353332653139336165613261316638666563
63346131306536353630363236613864393935333431333864333464353664366134313861633463
34336364663030376264663961396235643438653730396661643032623762623965623737326430
34333236316335373863663232356136333431666233353138353139313466343762356632396561
37356162303636343833316432353831373532326436363538333466333636306666626465666333
36653065363538653266643831326234356534303133343564646564323762653238303161336131
35396235343837333331396535646564656433343766323765333465356134323536636639623962
65633233376136316334653335623666313666376661326362663338633862643963353536616563
66323939643332663665306536356566383164356561303935313432373264353738643131653831
31346231633037616139373330316231363938386536303432386638323139326132663539663738
64636231663538333932363332663137323764346336633336363965616535373030663363363861
65363632653831353761306231663562623732353433656637353966303033636666613739306230
64363465623031386431663565623966643836383532626134366363656265646563343635383538
32346166363832626664613335383731616135376635336266326531663731373633303131373965
66663666333739373033616363313132643938656335353164343764383933636265656665663433
65386235636531653236333465316331353938323331623733623731303462306566333365383134
31356337343732613764633731306335366136346633393265666331636465323566656337323838
37393031316630623466393138636666303065623430656336386430363962336539616262633030
62616365623765366236303539353166306630393362386430363736313438346266366336323839
34376462376633366463636339646337623965633663363862393261373535636230613532386464
34393564383130306138366564366666646132363732653036353135656132656339353932383530
61636234393165326631303731633062333735306635303566373838393164306538303664313266
33346337613836636337316638323631396235643363333836323135303738366235326465376630
3363
37316466386337633433633632333065613137643963356437653030653433353464666636383238
6131616231356633383839653530666333343666316363630a323736656238336633336363656634
35633031623730613966343533393033386538346162333061613135353063323533336135376337
6262346462636361370a356336616330323738383535356238383238656366303833313137613461
39613266616632376433636263653332363661373832316538343530353963363762356534373264
38316134643632386564626662616235316563343833646164336439623035303838316630393030
32636566663835626362666235363432316430633139333935383031303564353633323835616530
39353336366634356530393339633836613563666435323533323731666666656339313431613161
63386639663635393339643965643664303130633234333130663336623738313665366630316539
39373930316434636261646431633666653066373632396136313639633266383036643739323936
63343836666263623966333364616230393338386439616264616437373739373366326430363038
61333237323139336236333366666164313364366663333335646139353061323362313465613865
32376234323936336466356239666162323330323133616166333733613566636635303965316665
61616135333964613862303432323032313533353132303762396539323033636636643539663334
36663866313130303531333961373337316462376564396639656265393663373230623937376337
34656339323864633462633030333730613765653631636233356535643333356661333331303935
33646364636434336162356535393162353533636262333735616463623432653933316230306261
38353766363130333963613830643735663930353162343665323435613435326638363162356438
35666666343165323561396336646264613633323066613665346535313165306435666463353030
33626661326536646264343531653834616563316533386539656431353165633161666366386433
66623662303636393732643330333664373935313932306232306433653038623461393030306562
66663637636534393631633939363037303331383166343561373163666263323563313636326538
31656132376636653336353666316639623230613563333363653965333435323231303161333231
65353539633132373036346233626533356139373938646130376437626532383864623138613463
65373530343563613633323439636638316330663639306438323761306238616539353262326166
61323239666661366665343736323834633065306237643538383238616563353366343936663835
66313066653832636534346136373962653136303435623365376235626463366633623561313231
66343861363336303136666136363531386166316631643565383835333261356237373736386638
32653565623034303939383134613235353237393035336662303432653734636630373235303733
37303234333764303863323733356363353864323038393762373339303239656566363966363832
66373965333362383031636330613637656436653365303930316233396431323338636239333930
62353637333562343534663964636235623363313463613135373639653536636339376435313662
31616465643730336562373463353965616263366137316430303666346532633735333661616337
31303361633437663065326666633437383534366639396661623336643565383336623232666563
64376136303637393330363263666631336461396163373932346138653362326163666331336434
66356232666439633132363537626436613064663562333536303238313834303666363830353632
33633633373738383165363135323232313735393335646238326261643765613061623264343261
37663531396165653838346431666534636365373563366636383165376137396537353031653261
62393437633735366433613166356337303564313538633933633465363331316332656262303439
62383535383666383535326538356431313365663937313065363138636336666631643737343634
61396664383539316261386235643430383436396638303737366632306538636133666239616162
33373066623365353832666139633436343664333434393933646265323132616665623938643061
32663763316639333036343333373632633431373038326666323036333664333332313731313865
33363638616661363763313263623230663735383834353835613861373063666538363764333433
65316333643937366632313335643364333736633936316132313534373862646365323037363963
63353237373730623533633961323038616532643034643939353034383036326461383566346230
61363561666265393536323164323462356237373764306632383034646665613130373030383336
39353235353036323964356139363530636364346666663331643736306361653966636164613830
65353664643733383737663766613965616334356561376538623763613331343334613532386562
33623564636233663234313631623734333934303439383662336665623936643039623039303962
63336433333766376130356339303239626536383364373063663435653236386139613366323938
34653161313061333837373164653238663539326562633237616134623366643039373033353331
31633436323463623661656636383964643362613130336537653731646239313365633662656132
35393262613830323964346662666664346664316662623865663236643065333533313135643063
37343461316564316666343465303539383463613663306136653465383336353232366238316332
31313837323533313863333565643762653362633231333436396238666562363139626336666532
38326331653736393031636264316330643532353862366235613334383266613238326431343066
33666566613061363465376637636636626432383132636330343736653034643965396566373064
37303830393531383131646161613335666631633535383739626130366464313930343262393533
66373137303531623663643339643233373737303831323735623561656165383034373131386639
32333838663264366661616262373439323433386538373261373465343163326438623962333566
30376463646631663330366364626133386131313261396434656336636566323234633864323731
61393864666537656338646462353764356131303836333762333932643130346439356432323231
65373866393263313236353937363031366661666664346362313466356366663336383761623637
66343938303839663630363562353634353938643638373335636530323430656336303963666662
63643031396133646531316162323836616461373863643463663132383764363034663562363365
37383234663939646539643562376532323936616264326533333637323633373330643162623537
63626138616165343938643237313562616234303665303635653830343735326661666235643333
36623333343365613161663663356566656533363736313231336536386339326336373730643630
61386139313064376161303066323162316564313761303734376663653464323138303339323763
35373930366262373936356464613731396136613634663562373737663739613562373333386265
35323161316633373430633833663266633030613430613635356135393161653836366464326236
63373738366661333964623962363035636131656539613139373138643839643138373564333430
34623932396134343762393334373033663132306636633864643235373861386633366639316566
66623439613730353237366366663161363030646663323836653638383330323164333461666334
62333662643631373361643263646537323239643234346333333261623933626166333630356662
61333362313230343134343331303031303634383335313339303363383930373136333336313062
39626532646365663734313462343766333962333266363965373630363831626633623830633733
37633666663361343639366663313237633161353062396337393138613261383935353730353932
32383066386636346433306662326130633662363135313331383530346632356333363238383266
61303138636664303637326137376638333266343132356237383432613238333632356163643232
32333966383164343064643934333765386136323736303138326635663561343964393934316433
37663763653034363039636436346630303664346237616433376462303463633930383430646430
38343762663635373033373135326361393935386566363031366630366561663431333461663332
62393538306163386139656431353235343265373033663530306163316464373432353661343439
35356634323966646263613937663234613431316333656330343765316163626339376430343064
30363335633337623139343464656431353738623230633137633130353430373435303963663333
66646563626336393538613134636162643864316163626361663663313131343063643336396532
36333336386130653537653637653163343031376436383763386163376336623837336365616530
66386262363633353465663664326537343037383838333863366662306131303364626538623763
30376132613861636539393138326131356333613837336365383862383362303233633333396238
34663163363234363564336338656366313931353262303032343334303864386337356437633361
39383366386334303039313439376236663533633035326662626466333539626261643466653966
66383862663635346539396139323138613264373436653338653136366135613962373562373932
62316134636636373264623365613636373132396433373630643531633536333130646130373963
33356538316334336236346532356263363933633933376165666563313161343638656365373137
61656438313063363661316537336538646233326564633934303766383738343130373737653961
32326462626435343363326366663364376431613837646365383933396437383138333665383832
33376563363466333637656233643137333636326535653335393931653063623937373836313863
30363238356466363461313633306565646336633366346461333032643635366336646365643761
37363433623037333838663731376663306134363665383835333164643131646632393639363538
32656539643461653861376436396130316166616265336433356663343235333763636564326464
32663530333030306130366532636439353264303137613366353266363664346433666565303135
34343835623936643631643166636339306231323138356565373663636665303264653161613539
65336230633035336464373431663130383131376430306136333634376435393933623238653136
61633138653339336631313932353264366334616630363464373131376538303630663566653061
30386665383864383838613835613337633937616437376363626263376634643061393937653430
38626539383634616536396665396262633666376438363930383635353936623534353131666333
31616536383662663666333265623032613936383063303636366362346337333337633739393665
62306639376466636161333233656239313964373239336134646434613166646534656466356431
36626162383866643633323938646130366261373366353764646234653236323166363463366435
65636137643639353039616138383265633832373864643438366333393031326363333461376138
65303838626435656634626236663338316632613664646463306561663532353062643637663866
32663236633630383365663235313337373837396531326532333632343736343035643733333262
61633532323330623338393838626333323932376630316239303335313765343039313332316336
32303638356539333136353163396536313830333264613266303139376465613166363730343635
37623532356135613831363137356238373863653933366532346532363130386363616133373764
62353066376631343934356337326465613436363361646431373962656639393566323731316136
30313730386438663261636562643061333664303636373732663464356530336333383739313566
35616566663065313732656532346330636335383664383061323131306530623632353132326432
34313365396430396338363063376534363332306432313531303261353736353138383733346363
37373063666630633632613039353763626165613038616464363333383662356363373133623261
61316130393165633434623334616239666137653831636131393732363330363039306666346363
38353832356138383732306533646133663865663062663862663232323038383532616230386265
38626535313931386533613936343637373064666663336436326464313834333334366264336130
37346438383739313735636139343638373063306162313230366364316136333137653939313032
63303035663465653062613764333434323234323332316365333165383533396430316537623164
37356138623233656436663738393966346662646339626137663533616234653535663663383738
37323564653664653530653533396332333664636261306339663830373730613832363565613363
63333635646339656535366365633638393164663562633562383162373932306233313164386331
37613036323664316237336130366138333863316563666337333464383932353363306231336338
65393537636162316637343262313333386532616632376563333039386131336230316266396265
34626630633038623462346365316438316238663163346136663436393736623464643961303339
35613565393633373861626239663563336633643565383833373632353737393838666161633462
36316634313530663032366430656131373838616161613265633936633930646137646661653738
31356266666162643736363538646464363164633434643161643239333336326262386135633165
65656334313632336138393364356664386237313936626333616638663666396530623464613665
36636537396565343864353465633638613439663663633766356331333036313130383432366136
66383333643938613831386631386537353166653561376161633431306531343033646233343731
34363164316463633133616230316433373966343031353733343762643032356264646631323236
33393231333330303339613634643333346134643035653330343230663035343230626465386339
36323030633037363462343130343333313538393163393263633339323133656239366161323837
30383231323061323538393063316666303534663664623464383634613834326237363532353165
30356561643738373537343831663461623839303363366266356663666664623030333936656436
39616261626464343039623161343234626566623537396264366439666438343739306361383666
33326433383830313933383030393235306261323462323265623862363861653331643736363336
36373561363566346232613631653464613034653438343462383432643962626665303634646230
63656261613463623466393064623534306363663835616138626332356430633165363366343461
66623133666461313934346365623434653433373734376632616435386636613166633935376337
61326437333132306637663436663930396461393131643766643362316434346438316630343833
31343638383339646436363238393634636363303861383266313638623663303732333231316366
37653861663737656631666535383366643265373135396236393936353264613765656534313430
39663431623737346462303035613162393939623239326232326330636562326236323830613238
32303263333864616434316361646162623163306262646131346135613162353833366537666532
39326430306331663736363365386264653634613530376639313336613939363135656638353632
31656430306363396366306539626436356436306233303032623265356337303666363965353964
62373031336438326635386632313365336236303737373832366363333634383835306231623065
62646236643264373036303465663838366565616530306463613563613463323735646430663566
34356263353637386163316639636334616663303935666530396333653633343861393335313966
36383139383835343937393934316330623338646663636466396436353739303464313161376138
61663033623031653361643735366562393431643236636531323061303033313966633838646237
3831653036383565396363663066316432636537376430376165

Some files were not shown because too many files have changed in this diff Show More