линтеры python перенастроены: mypy заменён на pyrefly

- набор правил ruff расширен по канону rp-local-env (ANN, PTH, C90, G и др.),
  в lefthook добавлен job ruff check --fix
- код приведён под новые правила: логирование через %s вместо f-строк,
  os.path заменён на pathlib, run_backup_process и initialize разбиты
  на функции по порогу цикломатической сложности
This commit is contained in:
av
2026-08-22 11:32:03 +03:00
parent 321f6e7b6b
commit bdc319df64
7 changed files with 266 additions and 253 deletions
+7 -4
View File
@@ -15,7 +15,7 @@ Ansible-проект для автоматизации личного серве
- `templates/` — общие шаблоны (например `env.template`). - `templates/` — общие шаблоны (например `env.template`).
- `scripts/` — вспомогательные Python-скрипты (SMTP-утилиты для Yandex Cloud Postbox). - `scripts/` — вспомогательные Python-скрипты (SMTP-утилиты для Yandex Cloud Postbox).
- `.gitea/workflows/lint.yml` — CI: yamllint + ansible-lint. - `.gitea/workflows/lint.yml` — CI: yamllint + ansible-lint.
- `lefthook.yml` — pre-commit хуки (ruff, mypy, yamllint, ansible-lint, gitleaks, проверка vault). - `lefthook.yml` — pre-commit хуки (ruff, pyrefly, yamllint, ansible-lint, gitleaks, проверка vault).
- `tasks.py` — задачи через invoke (`inv <task>`). - `tasks.py` — задачи через invoke (`inv <task>`).
- `pyproject.toml` — зависимости Python, управляются через `uv`. - `pyproject.toml` — зависимости Python, управляются через `uv`.
@@ -109,10 +109,13 @@ uv run ansible-galaxy install --role-file requirements.yml
## Линтинг и CI ## Линтинг и CI
- CI (`.gitea/workflows/lint.yml`): два параллельных job — yamllint и ansible-lint. - CI (`.gitea/workflows/lint.yml`): два параллельных job — yamllint и ansible-lint.
- Конфиги: `.yamllint.yml` (макс. длина строки 120), `.ansible-lint.yml` (профиль production, offline). - Конфиги: `.yamllint.yml` (макс. длина строки 120), `.ansible-lint.yml` (профиль production, offline),
`[tool.ruff.lint]` и `[tool.pyrefly]` в `pyproject.toml`.
- Набор правил ruff расширен относительно дефолтного (ANN, PTH, ERA, PT, C90, RET, N, Q, TID, G, LOG,
FURB, PLC) и синхронизирован с остальными репозиториями — канон настройки лежит в `rp-local-env`.
- Pre-commit хуки через lefthook: - Pre-commit хуки через lefthook:
- `ruff format` + `ruff check` — форматирование и линтинг Python. - `ruff format` + `ruff check --fix` + `ruff check` — форматирование и линтинг Python.
- `mypy` — проверка типов Python. - `pyrefly` — проверка типов Python (заменил mypy).
- `yamllint` — линтинг YAML. - `yamllint` — линтинг YAML.
- `ansible-lint` — линтинг Ansible (профиль production). - `ansible-lint` — линтинг Ansible (профиль production).
- `gitleaks` — поиск секретов в staged-файлах. - `gitleaks` — поиск секретов в staged-файлах.
+139 -98
View File
@@ -160,7 +160,7 @@ class Storage(ABC):
class ResticStorage(Storage): class ResticStorage(Storage):
TYPE_NAME = "restic" TYPE_NAME = "restic"
def __init__(self, name: str, params: Dict[str, Any]): def __init__(self, name: str, params: Dict[str, Any]) -> None:
self.name = name self.name = name
self.restic_repository = str(params.get("restic_repository", "")) self.restic_repository = str(params.get("restic_repository", ""))
self.restic_password = str(params.get("restic_password", "")) self.restic_password = str(params.get("restic_password", ""))
@@ -306,7 +306,7 @@ class Notifier(ABC):
class AppriseNotifier(Notifier): class AppriseNotifier(Notifier):
TYPE_NAME = "apprise" TYPE_NAME = "apprise"
def __init__(self, name: str, params: Dict[str, Any]): def __init__(self, name: str, params: Dict[str, Any]) -> None:
self.name = name self.name = name
self.api_url = str(params.get("api_url", "")).rstrip("/") self.api_url = str(params.get("api_url", "")).rstrip("/")
self.tag = str(params.get("tag", "")) self.tag = str(params.get("tag", ""))
@@ -329,12 +329,14 @@ class AppriseNotifier(Notifier):
logger.info("Apprise notification sent successfully") logger.info("Apprise notification sent successfully")
else: else:
logger.error( logger.error(
f"Failed to send Apprise notification: {response.status_code} - {response.text}" "Failed to send Apprise notification: %s - %s",
response.status_code,
response.text,
) )
class ApplicationFinder: class ApplicationFinder:
def __init__(self, roots: List[Path]): def __init__(self, roots: List[Path]) -> None:
self.roots = roots self.roots = roots
self.warnings: List[str] = [] self.warnings: List[str] = []
@@ -361,7 +363,7 @@ class ApplicationFinder:
) )
) )
except (KeyError, OSError) as e: except (KeyError, OSError) as e:
logger.warning(f"Could not get owner for {app_dir}: {e}") logger.warning("Could not get owner for %s: %s", app_dir, e)
applications.sort(key=lambda app: app.path.name) applications.sort(key=lambda app: app.path.name)
return applications return applications
@@ -373,9 +375,8 @@ class ApplicationFinder:
if script_path.exists(): if script_path.exists():
if os.access(script_path, os.X_OK): if os.access(script_path, os.X_OK):
return script_path return script_path
else:
logger.warning( logger.warning(
f"Backup script {script_path} exists but is not executable" "Backup script %s exists but is not executable", script_path
) )
return None return None
@@ -435,7 +436,7 @@ class BackupManager:
schedule: Schedule, schedule: Schedule,
maintenance: MaintenanceOptions, maintenance: MaintenanceOptions,
forced_phases: Optional[List[str]] = None, forced_phases: Optional[List[str]] = None,
): ) -> None:
self.errors: List[str] = [] self.errors: List[str] = []
self.warnings: List[str] = [] self.warnings: List[str] = []
self.backed_up_apps: List[str] = [] self.backed_up_apps: List[str] = []
@@ -452,20 +453,56 @@ class BackupManager:
def run_backup_process(self, applications: List[Application]) -> bool: def run_backup_process(self, applications: List[Application]) -> bool:
"""Main backup process""" """Main backup process"""
logger.info("Starting backup process") logger.info("Starting backup process")
logger.info(f"Found {len(applications)} application directories") logger.info("Found %d application directories", len(applications))
# Какие фазы выполняем в этот прогон: либо принудительно из CLI, либо по расписанию. self.active_phases = self._resolve_phases()
self._run_archive_phase(applications)
backup_dirs = self._collect_backup_dirs(applications)
overall_success = self._run_storages(backup_dirs)
self._send_notification(overall_success)
logger.info("Backup process completed")
if self.errors:
logger.error("Backup completed with %d errors", len(self.errors))
return False
if self.warnings:
logger.warning("Backup completed with %d warnings", len(self.warnings))
return True
logger.info("Backup completed successfully")
return True
def _resolve_phases(self) -> List[str]:
"""Какие фазы выполняем в этот прогон: принудительно из CLI или по расписанию."""
if self.forced_phases is not None: if self.forced_phases is not None:
self.active_phases = self.forced_phases logger.info("Phases (forced): %s", ", ".join(self.forced_phases))
logger.info("Phases (forced): %s", ", ".join(self.active_phases)) return self.forced_phases
else:
self.active_phases = self.schedule.due_phases(datetime.now())
logger.info("Phases (scheduled): %s", ", ".join(self.active_phases))
phases = self.schedule.due_phases(datetime.now())
logger.info("Phases (scheduled): %s", ", ".join(phases))
return phases
def _run_archive_phase(self, applications: List[Application]) -> None:
"""Прогнать скрипты дампов приложений и собрать список того, что уедет в restic.
Фаза нужна только вместе с restic backup: без неё дампы делать некому и незачем.
"""
archive_start = time.monotonic() archive_start = time.monotonic()
# Archive phase (per-app backup scripts) нужна только если будем делать restic backup.
if PHASE_BACKUP in self.active_phases: if PHASE_BACKUP in self.active_phases:
for app in applications: for app in applications:
self._archive_app(app)
else:
logger.info("Backup phase not active, skipping per-app archive scripts")
self.archive_duration = time.monotonic() - archive_start
logger.info(
"Archive phase finished in %s", format_duration(self.archive_duration)
)
def _archive_app(self, app: Application) -> None:
"""Обработать одно приложение: сделать дамп, если он предусмотрен."""
app_dir = str(app.path) app_dir = str(app.path)
username = app.owner username = app.owner
app_name = app.path.name app_name = app.path.name
@@ -475,8 +512,10 @@ class BackupManager:
# Приложение без дампа: restic забирает его данные как есть, # Приложение без дампа: restic забирает его данные как есть,
# отдельный шаг архивации ему не нужен. # отдельный шаг архивации ему не нужен.
logger.info( logger.info(
f"No backup script for app: {app_dir} (user {username}), " "No backup script for app: %s (user %s), "
f"data directories go to restic as is" "data directories go to restic as is",
app_dir,
username,
) )
self.backed_up_apps.append(app_name) self.backed_up_apps.append(app_name)
else: else:
@@ -486,35 +525,35 @@ class BackupManager:
) )
logger.warning(warning_msg) logger.warning(warning_msg)
self.warnings.append(warning_msg) self.warnings.append(warning_msg)
continue return
logger.info(f"Processing backup for app: {app_dir} (user {username})") logger.info("Processing backup for app: %s (user %s)", app_dir, username)
if not self._run_app_backup(str(app.backup_script), app_dir, username): if not self._run_app_backup(str(app.backup_script), app_dir, username):
continue return
# Дамп сделан, но в restic он попадёт только если есть цели бекапа; # Дамп сделан, но в restic он попадёт только если есть цели бекапа;
# об их отсутствии уже предупредил ApplicationFinder. # об их отсутствии уже предупредил ApplicationFinder.
if app.backup_targets: if app.backup_targets:
self.backed_up_apps.append(app_name) self.backed_up_apps.append(app_name)
else:
logger.info("Backup phase not active, skipping per-app archive scripts")
self.archive_duration = time.monotonic() - archive_start
logger.info(
"Archive phase finished in %s", format_duration(self.archive_duration)
)
# Collect backup directories from applications @staticmethod
def _collect_backup_dirs(applications: List[Application]) -> List[str]:
"""Собрать цели бекапа всех приложений, сохраняя порядок и убирая дубли."""
backup_dirs: List[str] = [] backup_dirs: List[str] = []
for app in applications: for app in applications:
for target in app.backup_targets: for target in app.backup_targets:
target_str = str(target) target_str = str(target)
if target_str not in backup_dirs: if target_str not in backup_dirs:
backup_dirs.append(target_str) backup_dirs.append(target_str)
logger.info(f"Found backup directories: {backup_dirs}") logger.info("Found backup directories: %s", backup_dirs)
return backup_dirs
def _run_storages(self, backup_dirs: List[str]) -> bool:
"""Прогнать активные фазы по всем хранилищам.
Хранилища независимы: падение одного не отменяет попытку для остальных.
"""
overall_success = True 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: for storage in self.storages:
storage_start = time.monotonic() storage_start = time.monotonic()
try: try:
@@ -547,28 +586,14 @@ class BackupManager:
error_msg += f": {backup_result.error}" error_msg += f": {backup_result.error}"
self.errors.append(error_msg) self.errors.append(error_msg)
# Determine overall success
overall_success = overall_success and backup_result.success overall_success = overall_success and backup_result.success
# Send notification return overall_success
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: def _run_app_backup(self, script_path: str, app_dir: str, username: str) -> bool:
"""Run backup script as the specified user""" """Run backup script as the specified user"""
try: try:
logger.info(f"Running backup script {script_path} (user {username})") logger.info("Running backup script %s (user %s)", script_path, username)
# Use su to run the script as the user # Use su to run the script as the user
cmd = ["su", "--login", username, "--command", script_path] cmd = ["su", "--login", username, "--command", script_path]
@@ -582,9 +607,8 @@ class BackupManager:
) )
if result.returncode == 0: if result.returncode == 0:
logger.info(f"Backup script for {username} completed successfully") logger.info("Backup script for %s completed successfully", username)
return True return True
else:
error_msg = f"Backup script {script_path} failed with return code {result.returncode}" error_msg = f"Backup script {script_path} failed with return code {result.returncode}"
if result.stderr: if result.stderr:
error_msg += f": {result.stderr}" error_msg += f": {result.stderr}"
@@ -644,7 +668,7 @@ class BackupManager:
try: try:
notificator.send(title, message) notificator.send(title, message)
except Exception as e: except Exception as e:
logger.error(f"Failed to send notification: {str(e)}") logger.error("Failed to send notification: %s", e)
def parse_phases(raw: str) -> List[str]: def parse_phases(raw: str) -> List[str]:
@@ -659,6 +683,65 @@ def parse_phases(raw: str) -> List[str]:
return [p for p in PHASE_ORDER if p in requested] return [p for p in PHASE_ORDER if p in requested]
def build_storages(raw_config: Dict[str, Any]) -> List[Storage]:
"""Собрать хранилища из секции [storage] конфига."""
storage_raw = raw_config.get("storage") or {}
storages: List[Storage] = []
for name, params in storage_raw.items():
if not isinstance(params, dict):
raise ValueError(f"Storage config for {name} must be a table")
if params.get("type", "") == ResticStorage.TYPE_NAME:
storages.append(ResticStorage(name, params))
if not storages:
raise ValueError("At least one storage backend must be configured")
return storages
def build_notifiers(raw_config: Dict[str, Any]) -> List[Notifier]:
"""Собрать нотификаторы из секции [notifier] конфига."""
notifications_raw = raw_config.get("notifier") or {}
notifiers: List[Notifier] = []
for name, params in notifications_raw.items():
if not isinstance(params, dict):
raise ValueError(f"Notificator config for {name} must be a table")
if params.get("type", "") == AppriseNotifier.TYPE_NAME:
notifiers.append(AppriseNotifier(name, params))
if not notifiers:
raise ValueError("At least one notification backend must be configured")
return notifiers
def build_schedule(raw_config: Dict[str, Any]) -> Schedule:
"""Собрать расписание обслуживающих фаз из секции [schedule] конфига."""
schedule_raw = raw_config.get("schedule") or {}
if not isinstance(schedule_raw, dict):
raise ValueError("'schedule' must be a table in config.toml")
return Schedule(
cron={
phase: str(schedule_raw[phase])
for phase in SCHEDULED_PHASES
if phase in schedule_raw
}
)
def build_maintenance(raw_config: Dict[str, Any]) -> MaintenanceOptions:
"""Собрать параметры обслуживания из секции [maintenance] конфига."""
maintenance_raw = raw_config.get("maintenance") or {}
if not isinstance(maintenance_raw, dict):
raise ValueError("'maintenance' must be a table in config.toml")
defaults = MaintenanceOptions()
return MaintenanceOptions(
verify_subset=str(maintenance_raw.get("verify_subset", defaults.verify_subset)),
prune_max_unused=str(
maintenance_raw.get("prune_max_unused", defaults.prune_max_unused)
),
prune_max_repack=str(
maintenance_raw.get("prune_max_repack", defaults.prune_max_repack)
),
)
def initialize( def initialize(
config_path: Path, config_path: Path,
forced_phases: Optional[List[str]] = None, forced_phases: Optional[List[str]] = None,
@@ -667,7 +750,7 @@ def initialize(
with config_path.open("rb") as config_file: with config_path.open("rb") as config_file:
raw_config = tomllib.load(config_file) raw_config = tomllib.load(config_file)
except OSError as e: except OSError as e:
logger.error(f"Failed to read config file {config_path}: {e}") logger.error("Failed to read config file %s: %s", config_path, e)
raise raise
host_name = str(raw_config.get("host_name", "unknown")) host_name = str(raw_config.get("host_name", "unknown"))
@@ -677,52 +760,10 @@ def initialize(
raise ValueError("roots must be a non-empty list of paths in config.toml") raise ValueError("roots must be a non-empty list of paths in config.toml")
roots = [Path(root) for root in roots_raw] roots = [Path(root) for root in roots_raw]
storage_raw = raw_config.get("storage") or {} storages = build_storages(raw_config)
storages: List[Storage] = [] notifiers = build_notifiers(raw_config)
for name, params in storage_raw.items(): schedule = build_schedule(raw_config)
if not isinstance(params, dict): maintenance = build_maintenance(raw_config)
raise ValueError(f"Storage config for {name} must be a table")
storage_type = params.get("type", "")
if storage_type == ResticStorage.TYPE_NAME:
storages.append(ResticStorage(name, params))
if not storages:
raise ValueError("At least one storage backend must be configured")
notifications_raw = raw_config.get("notifier") or {}
notifiers: List[Notifier] = []
for name, params in notifications_raw.items():
if not isinstance(params, dict):
raise ValueError(f"Notificator config for {name} must be a table")
notifier_type = params.get("type", "")
if notifier_type == AppriseNotifier.TYPE_NAME:
notifiers.append(AppriseNotifier(name, params))
if not notifiers:
raise ValueError("At least one notification backend must be configured")
schedule_raw = raw_config.get("schedule") or {}
if not isinstance(schedule_raw, dict):
raise ValueError("'schedule' must be a table in config.toml")
schedule = Schedule(
cron={
phase: str(schedule_raw[phase])
for phase in SCHEDULED_PHASES
if phase in schedule_raw
}
)
maintenance_raw = raw_config.get("maintenance") or {}
if not isinstance(maintenance_raw, dict):
raise ValueError("'maintenance' must be a table in config.toml")
defaults = MaintenanceOptions()
maintenance = MaintenanceOptions(
verify_subset=str(maintenance_raw.get("verify_subset", defaults.verify_subset)),
prune_max_unused=str(
maintenance_raw.get("prune_max_unused", defaults.prune_max_unused)
),
prune_max_repack=str(
maintenance_raw.get("prune_max_repack", defaults.prune_max_repack)
),
)
config = Config(host_name=host_name) config = Config(host_name=host_name)
app_finder = ApplicationFinder(roots) app_finder = ApplicationFinder(roots)
@@ -767,7 +808,7 @@ def main() -> None:
logger.info("Backup process interrupted by user") logger.info("Backup process interrupted by user")
sys.exit(130) sys.exit(130)
except Exception as e: except Exception as e:
logger.error(f"Unexpected error in backup process: {str(e)}") logger.error("Unexpected error in backup process: %s", e)
sys.exit(1) sys.exit(1)
+7 -5
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import argparse import argparse
import os
from pathlib import Path
def main() -> None: def main() -> None:
@@ -18,12 +19,13 @@ def main() -> None:
if args.keep < 0: if args.keep < 0:
parser.error("--keep value cannot be negative") parser.error("--keep value cannot be negative")
if not os.path.isdir(args.directory): directory = Path(args.directory)
if not directory.is_dir():
parser.error(f"Directory not found: {args.directory}") parser.error(f"Directory not found: {args.directory}")
# Get list of files (exclude subdirectories) # Get list of files (exclude subdirectories)
files = [] files = []
with os.scandir(args.directory) as entries: with os.scandir(directory) as entries:
for entry in entries: for entry in entries:
if entry.is_file(): if entry.is_file():
files.append(entry.name) files.append(entry.name)
@@ -36,9 +38,9 @@ def main() -> None:
# Delete files and print results # Delete files and print results
for filename in to_delete: for filename in to_delete:
filepath = os.path.join(args.directory, filename) filepath = directory / filename
try: try:
os.remove(filepath) filepath.unlink()
print(f"Deleted: {filename}") print(f"Deleted: {filename}")
except Exception as e: except Exception as e:
print(f"Error deleting {filename}: {str(e)}") print(f"Error deleting {filename}: {str(e)}")
+7 -2
View File
@@ -14,13 +14,18 @@ pre-commit:
run: "uv run ruff format {staged_files}" run: "uv run ruff format {staged_files}"
stage_fixed: true stage_fixed: true
- name: "fix python"
glob: "**/*.py"
run: "uv run ruff check --fix {staged_files}"
stage_fixed: true
- name: "check python" - name: "check python"
glob: "**/*.py" glob: "**/*.py"
run: "uv run ruff check {staged_files}" run: "uv run ruff check {staged_files}"
- name: "mypy" - name: "pyrefly"
glob: "**/*.py" glob: "**/*.py"
run: "uv run mypy {staged_files}" run: "uv run pyrefly check {staged_files}"
- name: "yamllint" - name: "yamllint"
glob: "**/*.{yml,yaml}" glob: "**/*.{yml,yaml}"
+38 -1
View File
@@ -9,10 +9,47 @@ dependencies = [
"ansible-lint>=25.12.2", "ansible-lint>=25.12.2",
"croniter>=6.0.0", "croniter>=6.0.0",
"invoke>=2.2.1", "invoke>=2.2.1",
"mypy>=1.19.1", "pyrefly>=1.2.0",
"requests>=2.32.5", "requests>=2.32.5",
"ruff>=0.15.2", "ruff>=0.15.2",
"types-croniter>=6.0.0", "types-croniter>=6.0.0",
"types-requests>=2.32.4.20260107", "types-requests>=2.32.4.20260107",
"yamllint>=1.37.1", "yamllint>=1.37.1",
] ]
[tool.ruff.lint]
extend-select = [
# Правила, выпавшие из набора по умолчанию в ruff 0.16, но нужные проекту:
# E402 — импорты не в начале файла; A001/A002 — теневание встроенных имён
# аргументами invoke-задач.
"E402",
"A001",
"A002",
# Полная типизация: скрипты уезжают на сервер и правятся редко,
# аннотации — единственная страховка.
"ANN",
# pathlib вместо os.path, отсутствие закомментированного кода, стиль
# pytest и ограничение цикломатической сложности.
"PTH",
"ERA",
"PT",
"C90",
# Наборы, по которым код уже чист: включены, чтобы так и оставалось.
"RET",
"N",
"Q",
"TID",
"G",
"LOG",
"FURB",
"PLC",
]
# Any в сигнатурах используется осознанно: разбор конфигов из TOML
# и параметры сторонних API без стабов типов.
ignore = ["ANN401"]
[tool.pyrefly]
# Без секции pyrefly ругается на отсутствие конфига; здесь же задаём,
# что проверять при запуске без аргументов.
project-includes = ["**/*.py"]
project-excludes = ["**/.venv/**", "**/galaxy.roles/**", "**/__pycache__/**"]
+21 -22
View File
@@ -5,6 +5,7 @@ import os
import re import re
import subprocess import subprocess
import sys import sys
from pathlib import Path
from invoke.context import Context from invoke.context import Context
from invoke.exceptions import Exit from invoke.exceptions import Exit
@@ -37,7 +38,7 @@ def _remote_host() -> str:
def _authelia_docker() -> str: def _authelia_docker() -> str:
"""Команда запуска authelia CLI на том же образе, что и задеплоенный сервис""" """Команда запуска authelia CLI на том же образе, что и задеплоенный сервис"""
pattern = re.compile(r"""image:\s*["']?(\S*authelia/authelia:[^"'\s]+)""") pattern = re.compile(r"""image:\s*["']?(\S*authelia/authelia:[^"'\s]+)""")
with open(AUTHELIA_COMPOSE_FILE, encoding="utf-8") as compose: with Path(AUTHELIA_COMPOSE_FILE).open(encoding="utf-8") as compose:
for line in compose: for line in compose:
match = pattern.search(line) match = pattern.search(line)
if match: if match:
@@ -56,7 +57,7 @@ def _rest_args() -> list[str]:
def _resolve_playbook(name: str) -> str: def _resolve_playbook(name: str) -> str:
candidates = [name, f"{name}.yml", f"playbook-{name}.yml"] candidates = [name, f"{name}.yml", f"playbook-{name}.yml"]
for candidate in candidates: for candidate in candidates:
if os.path.isfile(candidate): if Path(candidate).is_file():
return candidate return candidate
raise Exit( raise Exit(
f"Плейбук для '{name}' не найден. Проверял: {', '.join(candidates)}", code=1 f"Плейбук для '{name}' не найден. Проверял: {', '.join(candidates)}", code=1
@@ -141,16 +142,12 @@ def edit_encrypted(ctx: Context, path: str) -> None:
# Путь к канону — сосед по файловой системе (../ansible-roles/roles), # Путь к канону — сосед по файловой системе (../ansible-roles/roles),
# поэтому не зависит от текущей директории и одинаков везде. # поэтому не зависит от текущей директории и одинаков везде.
SHARED_ROLES_DIR = os.path.normpath( SHARED_ROLES_DIR = Path(__file__).resolve().parent.parent / "ansible-roles" / "roles"
os.path.join(
os.path.dirname(os.path.abspath(__file__)), "..", "ansible-roles", "roles"
)
)
SYNCED_ROLES = ["eget", "app_image"] SYNCED_ROLES = ["eget", "app_image"]
def _require_canon() -> None: def _require_canon() -> None:
if not os.path.isdir(SHARED_ROLES_DIR): if not SHARED_ROLES_DIR.is_dir():
raise Exit(f"Канон не найден: {SHARED_ROLES_DIR}", code=1) raise Exit(f"Канон не найден: {SHARED_ROLES_DIR}", code=1)
@@ -171,11 +168,13 @@ def roles_status(ctx: Context) -> None:
_require_canon() _require_canon()
drift = False drift = False
for role in SYNCED_ROLES: for role in SYNCED_ROLES:
canon = os.path.join(SHARED_ROLES_DIR, role) canon = SHARED_ROLES_DIR / role
if not os.path.isdir(canon): if not canon.is_dir():
print(f"{role}: missing in shared") print(f"{role}: missing in shared")
drift = True drift = True
elif subprocess.run(["diff", "-rq", f"roles/{role}", canon]).returncode != 0: elif (
subprocess.run(["diff", "-rq", f"roles/{role}", str(canon)]).returncode != 0
):
print(f"{role}: differs") print(f"{role}: differs")
drift = True drift = True
else: else:
@@ -189,10 +188,10 @@ def roles_pull(ctx: Context) -> None:
"""Канон → репозиторий: inv roles-pull [-- <role> ...]""" """Канон → репозиторий: inv roles-pull [-- <role> ...]"""
_require_canon() _require_canon()
for role in _roles_to_sync(): for role in _roles_to_sync():
src = os.path.join(SHARED_ROLES_DIR, role) src = SHARED_ROLES_DIR / role
if not os.path.isdir(src): if not src.is_dir():
raise Exit(f"Роль '{role}' отсутствует в каноне: {src}", code=1) raise Exit(f"Роль '{role}' отсутствует в каноне: {src}", code=1)
os.makedirs(f"roles/{role}", exist_ok=True) Path("roles", role).mkdir(parents=True, exist_ok=True)
ctx.run(f"rsync -a -i --delete {src}/ roles/{role}/") ctx.run(f"rsync -a -i --delete {src}/ roles/{role}/")
@@ -201,24 +200,24 @@ def roles_push(ctx: Context) -> None:
"""Репозиторий → канон, с подтверждением при расхождении: inv roles-push [-- <role> ...]""" """Репозиторий → канон, с подтверждением при расхождении: inv roles-push [-- <role> ...]"""
_require_canon() _require_canon()
for role in _roles_to_sync(): for role in _roles_to_sync():
local = f"roles/{role}" local = Path("roles", role)
canon = os.path.join(SHARED_ROLES_DIR, role) canon = SHARED_ROLES_DIR / role
if not os.path.isdir(local): if not local.is_dir():
raise Exit(f"Локальной роли нет: {local}", code=1) raise Exit(f"Локальной роли нет: {local}", code=1)
differs = ( differs = (
os.path.isdir(canon) canon.is_dir()
and subprocess.run(["diff", "-rq", local, canon]).returncode != 0 and subprocess.run(["diff", "-rq", str(local), str(canon)]).returncode != 0
) )
if differs: if differs:
# Защита от затирания более свежего канона устаревшей копией. # Защита от затирания более свежего канона устаревшей копией.
subprocess.run(["diff", "-r", canon, local]) subprocess.run(["diff", "-r", str(canon), str(local)])
answer = input( answer = input(
f"Канон '{role}' отличается. Перезаписать его копией из этого репо? [y/N] " f"Канон '{role}' отличается. Перезаписать его копией из этого репо? [y/N] "
) )
if answer.strip().lower() != "y": if answer.strip().lower() != "y":
print(f"{role}: пропущено") print(f"{role}: пропущено")
continue continue
os.makedirs(canon, exist_ok=True) canon.mkdir(parents=True, exist_ok=True)
ctx.run(f"rsync -a -i --delete {local}/ {canon}/") ctx.run(f"rsync -a -i --delete {local}/ {canon}/")
@@ -267,7 +266,7 @@ def authelia_validate_config(ctx: Context) -> None:
"""Отрендерить конфиг authelia из шаблона и проверить его""" """Отрендерить конфиг authelia из шаблона и проверить его"""
dest = "temp/configuration.yml" dest = "temp/configuration.yml"
# temp/ в .gitignore, на свежем клоне его нет — ansible сам директорию не создаёт. # temp/ в .gitignore, на свежем клоне его нет — ansible сам директорию не создаёт.
os.makedirs(os.path.dirname(dest), exist_ok=True) Path(dest).parent.mkdir(parents=True, exist_ok=True)
try: try:
ctx.run( ctx.run(
"uv run ansible localhost" "uv run ansible localhost"
Generated
+21 -95
View File
@@ -415,66 +415,6 @@ 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" }, { 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]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "3.0.3" version = "3.0.3"
@@ -538,39 +478,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, { 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]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "1.1.0" version = "1.1.0"
@@ -607,7 +514,7 @@ dependencies = [
{ name = "ansible-lint" }, { name = "ansible-lint" },
{ name = "croniter" }, { name = "croniter" },
{ name = "invoke" }, { name = "invoke" },
{ name = "mypy" }, { name = "pyrefly" },
{ name = "requests" }, { name = "requests" },
{ name = "ruff" }, { name = "ruff" },
{ name = "types-croniter" }, { name = "types-croniter" },
@@ -621,7 +528,7 @@ requires-dist = [
{ name = "ansible-lint", specifier = ">=25.12.2" }, { name = "ansible-lint", specifier = ">=25.12.2" },
{ name = "croniter", specifier = ">=6.0.0" }, { name = "croniter", specifier = ">=6.0.0" },
{ name = "invoke", specifier = ">=2.2.1" }, { name = "invoke", specifier = ">=2.2.1" },
{ name = "mypy", specifier = ">=1.19.1" }, { name = "pyrefly", specifier = ">=1.2.0" },
{ name = "requests", specifier = ">=2.32.5" }, { name = "requests", specifier = ">=2.32.5" },
{ name = "ruff", specifier = ">=0.15.2" }, { name = "ruff", specifier = ">=0.15.2" },
{ name = "types-croniter", specifier = ">=6.0.0" }, { name = "types-croniter", specifier = ">=6.0.0" },
@@ -647,6 +554,25 @@ 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" }, { 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 = "pyrefly"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/89/01/a86e9f24722b095c3f88e3616132b75a21b0df53804bdc6a45314dd4d93c/pyrefly-1.2.0.tar.gz", hash = "sha256:5485f960fc2481617068c918335c39ab1507ef90b6b5bd35bf57726e60e73185", size = 6243654, upload-time = "2026-08-01T02:56:27.592Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/9d/3c0ef1d4843987b22f996ed381ec9cf5a3b1273e29804db276252e4c95eb/pyrefly-1.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7f46d983ac49ddd2b043694960a01dc6a19a5cfd8eec609d6bd9c42866f91b4e", size = 14026305, upload-time = "2026-08-01T02:56:02.611Z" },
{ url = "https://files.pythonhosted.org/packages/0a/06/03bbb78fbea54cdc65b626619f3597d5611aca4fdef11e72a4e8360e7e63/pyrefly-1.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:756f669b5555090f5c1a4fef30db1785fabe657764f7e4e6dc88994dfb8ca82d", size = 13463880, upload-time = "2026-08-01T02:56:04.93Z" },
{ url = "https://files.pythonhosted.org/packages/13/5a/7d8bc00a38e93bbc9c3e7bd14d305f7948717e667c9bcddeab9dd42fd255/pyrefly-1.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3465812ce5ef4781fb592edbf2724547296f0a3124be115d73c7e8b2401862d", size = 13907329, upload-time = "2026-08-01T02:56:07.104Z" },
{ url = "https://files.pythonhosted.org/packages/be/94/9e08b4bf799d0b8f36b55a2783c7ba5f51730cf0632a85a67b5b5ed876cd/pyrefly-1.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5de7b2ad2bba5c8055181681a84b74143eac2234a48ba5d1b7ed7e7a722b02bd", size = 15039020, upload-time = "2026-08-01T02:56:09.208Z" },
{ url = "https://files.pythonhosted.org/packages/5b/bd/bca5fd0c80f4daf8ee6903a29df9f3de1feb05ff0946b8f35ec8c5096b13/pyrefly-1.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25822ea9505f589ea8a725e4268b475132fb89e038fbf092e446510443ac142a", size = 14986199, upload-time = "2026-08-01T02:56:11.924Z" },
{ url = "https://files.pythonhosted.org/packages/97/f7/f07087f3d185ad2eced0c56cef89ca5474dfb4ff25f146cd50a861c97553/pyrefly-1.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90efe75e17491ef5d636e10469e9278d7d0256b3b4c5e1f4750069bf3ae0f5d1", size = 14393715, upload-time = "2026-08-01T02:56:14.143Z" },
{ url = "https://files.pythonhosted.org/packages/d3/70/0d142c320e284b9e3ce35e9b1e58b8ce2ee1f578f2a7234bc30e5022b94f/pyrefly-1.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:368aaf7eee4f511ddc0f8e564cf14e01ab2f10b0db9105c6d5b153bf498d07bf", size = 13933008, upload-time = "2026-08-01T02:56:16.525Z" },
{ url = "https://files.pythonhosted.org/packages/5d/e8/e84f11b6e1f63fd453ad3654213b9a0f6f4de8cef6b58038eef2d0d5955d/pyrefly-1.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52d5da7bc65fb7675fbaa80eda879d4f8787c494f04cac21603330d3abbdbbe", size = 14431827, upload-time = "2026-08-01T02:56:18.645Z" },
{ url = "https://files.pythonhosted.org/packages/0f/06/810d31380f66c75e1c0779a408d3b16117b1b368b57894f6aa66bef21686/pyrefly-1.2.0-py3-none-win32.whl", hash = "sha256:8c90751de8506d938e8f802659c74cf35bd7a0036510ee6c634a38eebb280bfa", size = 13229447, upload-time = "2026-08-01T02:56:20.921Z" },
{ url = "https://files.pythonhosted.org/packages/ed/98/4dafa3c7a1caed2dc8cc708dde09ba27963c7736508f55b626fff3024113/pyrefly-1.2.0-py3-none-win_amd64.whl", hash = "sha256:8a8964c224ccc4882730130955815de21ff443c1ac3f0b90685b19bf63848170", size = 14087387, upload-time = "2026-08-01T02:56:23.188Z" },
{ url = "https://files.pythonhosted.org/packages/1b/1c/df3cb0a2e5591660ded7a1836cd2f29dc48c91adb1c0a3a700a96f6d09e1/pyrefly-1.2.0-py3-none-win_arm64.whl", hash = "sha256:3a90bb8df39dfbac74b1f3b2e9d7c526b8f80568884c3944d955023a73ebf61e", size = 13430873, upload-time = "2026-08-01T02:56:25.425Z" },
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"