в ruff включён набор UP: современный синтаксис аннотаций
- typing.Dict/List заменены на dict/list, Optional[X] — на X | None; эти же места подсвечивал basedpyright в редакторе - из smtp-convert-secret-key-to-password.py убрана проверка на Python 2
This commit is contained in:
+52
-54
@@ -26,7 +26,7 @@ from abc import ABC
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from croniter import croniter
|
from croniter import croniter
|
||||||
@@ -87,9 +87,9 @@ class MaintenanceOptions:
|
|||||||
class Schedule:
|
class Schedule:
|
||||||
"""Расписание обслуживающих фаз: фаза -> cron-выражение."""
|
"""Расписание обслуживающих фаз: фаза -> cron-выражение."""
|
||||||
|
|
||||||
cron: Dict[str, str] = field(default_factory=dict)
|
cron: dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
def due_phases(self, now: datetime) -> List[str]:
|
def due_phases(self, now: datetime) -> list[str]:
|
||||||
"""Фазы, которые нужно выполнить в этот прогон, в порядке PHASE_ORDER."""
|
"""Фазы, которые нужно выполнить в этот прогон, в порядке PHASE_ORDER."""
|
||||||
phases = list(ALWAYS_PHASES)
|
phases = list(ALWAYS_PHASES)
|
||||||
for phase in SCHEDULED_PHASES:
|
for phase in SCHEDULED_PHASES:
|
||||||
@@ -114,14 +114,14 @@ class Schedule:
|
|||||||
class Application:
|
class Application:
|
||||||
path: Path
|
path: Path
|
||||||
owner: str
|
owner: str
|
||||||
backup_script: Optional[Path]
|
backup_script: Path | None
|
||||||
backup_targets: List[Path]
|
backup_targets: list[Path]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BackupResult:
|
class BackupResult:
|
||||||
success: bool
|
success: bool
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -129,7 +129,7 @@ class StorageRunResult:
|
|||||||
name: str
|
name: str
|
||||||
success: bool
|
success: bool
|
||||||
duration: float
|
duration: float
|
||||||
phases: List[str]
|
phases: list[str]
|
||||||
|
|
||||||
|
|
||||||
def format_duration(seconds: float) -> str:
|
def format_duration(seconds: float) -> str:
|
||||||
@@ -149,8 +149,8 @@ class Storage(ABC):
|
|||||||
|
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
backup_dirs: List[str],
|
backup_dirs: list[str],
|
||||||
phases: List[str],
|
phases: list[str],
|
||||||
maintenance: MaintenanceOptions,
|
maintenance: MaintenanceOptions,
|
||||||
) -> BackupResult:
|
) -> BackupResult:
|
||||||
"""Run the requested phases against this storage."""
|
"""Run the requested phases against this storage."""
|
||||||
@@ -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]) -> None:
|
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", ""))
|
||||||
@@ -170,7 +170,7 @@ class ResticStorage(Storage):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"'env' must be a table for storage backend ResticStorage: '{self.name}'"
|
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()}
|
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:
|
if not self.restic_repository or not self.restic_password:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -179,8 +179,8 @@ class ResticStorage(Storage):
|
|||||||
|
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
backup_dirs: List[str],
|
backup_dirs: list[str],
|
||||||
phases: List[str],
|
phases: list[str],
|
||||||
maintenance: MaintenanceOptions,
|
maintenance: MaintenanceOptions,
|
||||||
) -> BackupResult:
|
) -> BackupResult:
|
||||||
try:
|
try:
|
||||||
@@ -191,12 +191,12 @@ class ResticStorage(Storage):
|
|||||||
|
|
||||||
def __build_steps(
|
def __build_steps(
|
||||||
self,
|
self,
|
||||||
backup_dirs: List[str],
|
backup_dirs: list[str],
|
||||||
phases: List[str],
|
phases: list[str],
|
||||||
maintenance: MaintenanceOptions,
|
maintenance: MaintenanceOptions,
|
||||||
) -> List[tuple[str, List[str]]]:
|
) -> list[tuple[str, list[str]]]:
|
||||||
"""Собрать restic-команды для запрошенных фаз в порядке PHASE_ORDER."""
|
"""Собрать restic-команды для запрошенных фаз в порядке PHASE_ORDER."""
|
||||||
steps: List[tuple[str, List[str]]] = []
|
steps: list[tuple[str, list[str]]] = []
|
||||||
|
|
||||||
for phase in PHASE_ORDER:
|
for phase in PHASE_ORDER:
|
||||||
if phase not in phases:
|
if phase not in phases:
|
||||||
@@ -261,8 +261,8 @@ class ResticStorage(Storage):
|
|||||||
|
|
||||||
def __run_internal(
|
def __run_internal(
|
||||||
self,
|
self,
|
||||||
backup_dirs: List[str],
|
backup_dirs: list[str],
|
||||||
phases: List[str],
|
phases: list[str],
|
||||||
maintenance: MaintenanceOptions,
|
maintenance: MaintenanceOptions,
|
||||||
) -> BackupResult:
|
) -> BackupResult:
|
||||||
logger.info("Starting restic run for storage '%s'", self.name)
|
logger.info("Starting restic run for storage '%s'", self.name)
|
||||||
@@ -283,9 +283,7 @@ class ResticStorage(Storage):
|
|||||||
|
|
||||||
return BackupResult(success=True)
|
return BackupResult(success=True)
|
||||||
|
|
||||||
def __run_step(
|
def __run_step(self, step: str, cmd: list[str], env: dict[str, str]) -> str | None:
|
||||||
self, step: str, cmd: List[str], env: Dict[str, str]
|
|
||||||
) -> Optional[str]:
|
|
||||||
"""Run a single restic command. Return None on success or error text."""
|
"""Run a single restic command. Return None on success or error text."""
|
||||||
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
|
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
|
||||||
|
|
||||||
@@ -306,7 +304,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]) -> None:
|
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", ""))
|
||||||
@@ -336,13 +334,13 @@ class AppriseNotifier(Notifier):
|
|||||||
|
|
||||||
|
|
||||||
class ApplicationFinder:
|
class ApplicationFinder:
|
||||||
def __init__(self, roots: List[Path]) -> None:
|
def __init__(self, roots: list[Path]) -> None:
|
||||||
self.roots = roots
|
self.roots = roots
|
||||||
self.warnings: List[str] = []
|
self.warnings: list[str] = []
|
||||||
|
|
||||||
def find_applications(self) -> List[Application]:
|
def find_applications(self) -> list[Application]:
|
||||||
"""Discover all applications with their backup scripts and targets."""
|
"""Discover all applications with their backup scripts and targets."""
|
||||||
applications: List[Application] = []
|
applications: list[Application] = []
|
||||||
source_dirs = itertools.chain(*(root.iterdir() for root in self.roots))
|
source_dirs = itertools.chain(*(root.iterdir() for root in self.roots))
|
||||||
|
|
||||||
for app_dir in source_dirs:
|
for app_dir in source_dirs:
|
||||||
@@ -368,7 +366,7 @@ class ApplicationFinder:
|
|||||||
applications.sort(key=lambda app: app.path.name)
|
applications.sort(key=lambda app: app.path.name)
|
||||||
return applications
|
return applications
|
||||||
|
|
||||||
def _find_backup_script(self, app_dir: Path) -> Optional[Path]:
|
def _find_backup_script(self, app_dir: Path) -> Path | None:
|
||||||
"""Find executable backup script in application directory."""
|
"""Find executable backup script in application directory."""
|
||||||
for name in ("backup.sh", "backup"):
|
for name in ("backup.sh", "backup"):
|
||||||
script_path = app_dir / name
|
script_path = app_dir / name
|
||||||
@@ -380,10 +378,10 @@ class ApplicationFinder:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _find_backup_targets(self, app_dir: Path) -> List[Path]:
|
def _find_backup_targets(self, app_dir: Path) -> list[Path]:
|
||||||
"""Resolve backup target directories for an application."""
|
"""Resolve backup target directories for an application."""
|
||||||
targets_file = app_dir / BACKUP_TARGETS_FILE
|
targets_file = app_dir / BACKUP_TARGETS_FILE
|
||||||
resolved_targets: List[Path] = []
|
resolved_targets: list[Path] = []
|
||||||
|
|
||||||
if targets_file.exists():
|
if targets_file.exists():
|
||||||
for target_line in self._parse_targets_file(targets_file):
|
for target_line in self._parse_targets_file(targets_file):
|
||||||
@@ -411,9 +409,9 @@ class ApplicationFinder:
|
|||||||
|
|
||||||
return resolved_targets
|
return resolved_targets
|
||||||
|
|
||||||
def _parse_targets_file(self, targets_file: Path) -> List[str]:
|
def _parse_targets_file(self, targets_file: Path) -> list[str]:
|
||||||
"""Parse backup-targets file, skipping comments and empty lines."""
|
"""Parse backup-targets file, skipping comments and empty lines."""
|
||||||
targets: List[str] = []
|
targets: list[str] = []
|
||||||
try:
|
try:
|
||||||
for raw_line in targets_file.read_text(encoding="utf-8").splitlines():
|
for raw_line in targets_file.read_text(encoding="utf-8").splitlines():
|
||||||
line = raw_line.strip()
|
line = raw_line.strip()
|
||||||
@@ -431,26 +429,26 @@ class BackupManager:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: Config,
|
config: Config,
|
||||||
storages: List[Storage],
|
storages: list[Storage],
|
||||||
notifiers: List[Notifier],
|
notifiers: list[Notifier],
|
||||||
schedule: Schedule,
|
schedule: Schedule,
|
||||||
maintenance: MaintenanceOptions,
|
maintenance: MaintenanceOptions,
|
||||||
forced_phases: Optional[List[str]] = None,
|
forced_phases: list[str] | None = None,
|
||||||
) -> 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] = []
|
||||||
self.config = config
|
self.config = config
|
||||||
self.storages = storages
|
self.storages = storages
|
||||||
self.notifiers = notifiers
|
self.notifiers = notifiers
|
||||||
self.schedule = schedule
|
self.schedule = schedule
|
||||||
self.maintenance = maintenance
|
self.maintenance = maintenance
|
||||||
self.forced_phases = forced_phases
|
self.forced_phases = forced_phases
|
||||||
self.active_phases: List[str] = []
|
self.active_phases: list[str] = []
|
||||||
self.archive_duration: float = 0.0
|
self.archive_duration: float = 0.0
|
||||||
self.storage_results: List[StorageRunResult] = []
|
self.storage_results: list[StorageRunResult] = []
|
||||||
|
|
||||||
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("Found %d application directories", len(applications))
|
logger.info("Found %d application directories", len(applications))
|
||||||
@@ -473,7 +471,7 @@ class BackupManager:
|
|||||||
logger.info("Backup completed successfully")
|
logger.info("Backup completed successfully")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _resolve_phases(self) -> List[str]:
|
def _resolve_phases(self) -> list[str]:
|
||||||
"""Какие фазы выполняем в этот прогон: принудительно из CLI или по расписанию."""
|
"""Какие фазы выполняем в этот прогон: принудительно из CLI или по расписанию."""
|
||||||
if self.forced_phases is not None:
|
if self.forced_phases is not None:
|
||||||
logger.info("Phases (forced): %s", ", ".join(self.forced_phases))
|
logger.info("Phases (forced): %s", ", ".join(self.forced_phases))
|
||||||
@@ -483,7 +481,7 @@ class BackupManager:
|
|||||||
logger.info("Phases (scheduled): %s", ", ".join(phases))
|
logger.info("Phases (scheduled): %s", ", ".join(phases))
|
||||||
return phases
|
return phases
|
||||||
|
|
||||||
def _run_archive_phase(self, applications: List[Application]) -> None:
|
def _run_archive_phase(self, applications: list[Application]) -> None:
|
||||||
"""Прогнать скрипты дампов приложений и собрать список того, что уедет в restic.
|
"""Прогнать скрипты дампов приложений и собрать список того, что уедет в restic.
|
||||||
|
|
||||||
Фаза нужна только вместе с restic backup: без неё дампы делать некому и незачем.
|
Фаза нужна только вместе с restic backup: без неё дампы делать некому и незачем.
|
||||||
@@ -536,9 +534,9 @@ class BackupManager:
|
|||||||
self.backed_up_apps.append(app_name)
|
self.backed_up_apps.append(app_name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _collect_backup_dirs(applications: List[Application]) -> List[str]:
|
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)
|
||||||
@@ -547,7 +545,7 @@ class BackupManager:
|
|||||||
logger.info("Found backup directories: %s", backup_dirs)
|
logger.info("Found backup directories: %s", backup_dirs)
|
||||||
return backup_dirs
|
return backup_dirs
|
||||||
|
|
||||||
def _run_storages(self, backup_dirs: List[str]) -> bool:
|
def _run_storages(self, backup_dirs: list[str]) -> bool:
|
||||||
"""Прогнать активные фазы по всем хранилищам.
|
"""Прогнать активные фазы по всем хранилищам.
|
||||||
|
|
||||||
Хранилища независимы: падение одного не отменяет попытку для остальных.
|
Хранилища независимы: падение одного не отменяет попытку для остальных.
|
||||||
@@ -671,7 +669,7 @@ class BackupManager:
|
|||||||
logger.error("Failed to send notification: %s", e)
|
logger.error("Failed to send notification: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def parse_phases(raw: str) -> List[str]:
|
def parse_phases(raw: str) -> list[str]:
|
||||||
"""Разобрать CLI-список фаз, вернуть их в порядке PHASE_ORDER."""
|
"""Разобрать CLI-список фаз, вернуть их в порядке PHASE_ORDER."""
|
||||||
requested = {p.strip() for p in raw.split(",") if p.strip()}
|
requested = {p.strip() for p in raw.split(",") if p.strip()}
|
||||||
unknown = requested - set(PHASE_ORDER)
|
unknown = requested - set(PHASE_ORDER)
|
||||||
@@ -683,10 +681,10 @@ 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]:
|
def build_storages(raw_config: dict[str, Any]) -> list[Storage]:
|
||||||
"""Собрать хранилища из секции [storage] конфига."""
|
"""Собрать хранилища из секции [storage] конфига."""
|
||||||
storage_raw = raw_config.get("storage") or {}
|
storage_raw = raw_config.get("storage") or {}
|
||||||
storages: List[Storage] = []
|
storages: list[Storage] = []
|
||||||
for name, params in storage_raw.items():
|
for name, params in storage_raw.items():
|
||||||
if not isinstance(params, dict):
|
if not isinstance(params, dict):
|
||||||
raise ValueError(f"Storage config for {name} must be a table")
|
raise ValueError(f"Storage config for {name} must be a table")
|
||||||
@@ -697,10 +695,10 @@ def build_storages(raw_config: Dict[str, Any]) -> List[Storage]:
|
|||||||
return storages
|
return storages
|
||||||
|
|
||||||
|
|
||||||
def build_notifiers(raw_config: Dict[str, Any]) -> List[Notifier]:
|
def build_notifiers(raw_config: dict[str, Any]) -> list[Notifier]:
|
||||||
"""Собрать нотификаторы из секции [notifier] конфига."""
|
"""Собрать нотификаторы из секции [notifier] конфига."""
|
||||||
notifications_raw = raw_config.get("notifier") or {}
|
notifications_raw = raw_config.get("notifier") or {}
|
||||||
notifiers: List[Notifier] = []
|
notifiers: list[Notifier] = []
|
||||||
for name, params in notifications_raw.items():
|
for name, params in notifications_raw.items():
|
||||||
if not isinstance(params, dict):
|
if not isinstance(params, dict):
|
||||||
raise ValueError(f"Notificator config for {name} must be a table")
|
raise ValueError(f"Notificator config for {name} must be a table")
|
||||||
@@ -711,7 +709,7 @@ def build_notifiers(raw_config: Dict[str, Any]) -> List[Notifier]:
|
|||||||
return notifiers
|
return notifiers
|
||||||
|
|
||||||
|
|
||||||
def build_schedule(raw_config: Dict[str, Any]) -> Schedule:
|
def build_schedule(raw_config: dict[str, Any]) -> Schedule:
|
||||||
"""Собрать расписание обслуживающих фаз из секции [schedule] конфига."""
|
"""Собрать расписание обслуживающих фаз из секции [schedule] конфига."""
|
||||||
schedule_raw = raw_config.get("schedule") or {}
|
schedule_raw = raw_config.get("schedule") or {}
|
||||||
if not isinstance(schedule_raw, dict):
|
if not isinstance(schedule_raw, dict):
|
||||||
@@ -725,7 +723,7 @@ def build_schedule(raw_config: Dict[str, Any]) -> Schedule:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_maintenance(raw_config: Dict[str, Any]) -> MaintenanceOptions:
|
def build_maintenance(raw_config: dict[str, Any]) -> MaintenanceOptions:
|
||||||
"""Собрать параметры обслуживания из секции [maintenance] конфига."""
|
"""Собрать параметры обслуживания из секции [maintenance] конфига."""
|
||||||
maintenance_raw = raw_config.get("maintenance") or {}
|
maintenance_raw = raw_config.get("maintenance") or {}
|
||||||
if not isinstance(maintenance_raw, dict):
|
if not isinstance(maintenance_raw, dict):
|
||||||
@@ -744,7 +742,7 @@ def build_maintenance(raw_config: Dict[str, Any]) -> MaintenanceOptions:
|
|||||||
|
|
||||||
def initialize(
|
def initialize(
|
||||||
config_path: Path,
|
config_path: Path,
|
||||||
forced_phases: Optional[List[str]] = None,
|
forced_phases: list[str] | None = None,
|
||||||
) -> tuple[ApplicationFinder, BackupManager]:
|
) -> tuple[ApplicationFinder, BackupManager]:
|
||||||
try:
|
try:
|
||||||
with config_path.open("rb") as config_file:
|
with config_path.open("rb") as config_file:
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ extend-select = [
|
|||||||
"ERA",
|
"ERA",
|
||||||
"PT",
|
"PT",
|
||||||
"C90",
|
"C90",
|
||||||
|
# Современный синтаксис: dict/list вместо typing.Dict/List, `X | None`
|
||||||
|
# вместо Optional. Те же места подсвечивает basedpyright в редакторе.
|
||||||
|
"UP",
|
||||||
# Наборы, по которым код уже чист: включены, чтобы так и оставалось.
|
# Наборы, по которым код уже чист: включены, чтобы так и оставалось.
|
||||||
"RET",
|
"RET",
|
||||||
"N",
|
"N",
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import hmac
|
|||||||
import hashlib
|
import hashlib
|
||||||
import base64
|
import base64
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
# These values are required to calculate the signature. Do not change them.
|
# These values are required to calculate the signature. Do not change them.
|
||||||
@@ -55,9 +54,6 @@ def calculate_key(secret_access_key: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if sys.version_info[0] < 3:
|
|
||||||
raise Exception("Must be using Python 3")
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Convert a Secret Access Key to an SMTP password."
|
description="Convert a Secret Access Key to an SMTP password."
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user