backup worked

This commit is contained in:
Volobuev Andrey
2025-10-06 13:59:30 +03:00
parent 2f8aea3620
commit 8f6b44c679
7 changed files with 1144 additions and 480 deletions

View File

@@ -1,88 +1,205 @@
# [MODULE] Superset Tool Logger Utility
# PURPOSE: Предоставляет стандартизированный класс-обертку `SupersetLogger` для настройки и использования логирования в проекте.
# COHERENCE: Модуль согласован со стандартной библиотекой `logging`, расширяя ее для нужд проекта.
# [MODULE_PATH] superset_tool.utils.logger
# [FILE] logger.py
# [SEMANTICS] logging, utils, aifriendly, infrastructure
# --------------------------------------------------------------
# [IMPORTS]
# --------------------------------------------------------------
import logging
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
from typing import Optional, Any, Mapping
# [END_IMPORTS]
# CONTRACT:
# PURPOSE: Обеспечивает унифицированную настройку логгера с выводом в консоль и/или файл.
# PRECONDITIONS:
# - `name` должен быть строкой.
# - `level` должен быть валидным уровнем логирования (например, `logging.INFO`).
# POSTCONDITIONS:
# - Создает и настраивает логгер с указанным именем и уровнем.
# - Добавляет обработчики для вывода в файл (если указан `log_dir`) и в консоль (если `console=True`).
# - Очищает все предыдущие обработчики для данного логгера, чтобы избежать дублирования.
# PARAMETERS:
# - name: str - Имя логгера.
# - log_dir: Optional[Path] - Директория для сохранения лог-файлов.
# - level: int - Уровень логирования.
# - console: bool - Флаг для включения вывода в консоль.
# --------------------------------------------------------------
# [ENTITY: Service('SupersetLogger')]
# --------------------------------------------------------------
"""
:purpose: Универсальная обёртка над ``logging.Logger``. Позволяет:
• задавать уровень и вывод в консоль/файл,
• передавать произвольные ``extra``‑поля,
• использовать привычный API (info, debug, warning, error,
critical, exception) без «падения» при неверных аргументах.
:preconditions:
- ``name`` строка‑идентификатор логгера,
- ``level`` валидный уровень из ``logging``,
- ``log_dir`` при указании директория, куда будет писаться файл‑лог.
:postconditions:
- Создан полностью сконфигурированный ``logging.Logger`` без
дублирующих обработчиков.
"""
class SupersetLogger:
"""
:ivar logging.Logger logger: Внутренний стандартный логгер.
:ivar bool propagate: Отключаем наследование записей, чтобы
сообщения не «проваливались» выше.
"""
# --------------------------------------------------------------
# [ENTITY: Method('__init__')]
# --------------------------------------------------------------
"""
:purpose: Конфигурировать базовый логгер, добавить обработчики
консоли и/или файла, очистить прежние обработчики.
:preconditions: Параметры валидны.
:postconditions: ``self.logger`` готов к использованию.
"""
def __init__(
self,
name: str = "superset_tool",
log_dir: Optional[Path] = None,
level: int = logging.INFO,
console: bool = True
):
console: bool = True,
) -> None:
self.logger = logging.getLogger(name)
self.logger.setLevel(level)
self.logger.propagate = False # ← не «прокидываем» записи выше
formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s'
)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
# [ANCHOR] HANDLER_RESET
# Очищаем существующие обработчики, чтобы избежать дублирования вывода при повторной инициализации.
# ---- Очистка предыдущих обработчиков (важно при повторных инициализациях) ----
if self.logger.hasHandlers():
self.logger.handlers.clear()
# [ANCHOR] FILE_HANDLER
# ---- Файловый обработчик (если указана директория) ----
if log_dir:
log_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d")
file_handler = logging.FileHandler(
log_dir / f"{name}_{timestamp}.log", encoding='utf-8'
log_dir / f"{name}_{timestamp}.log", encoding="utf-8"
)
file_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
# [ANCHOR] CONSOLE_HANDLER
# ---- Консольный обработчик ----
if console:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
self.logger.addHandler(console_handler)
# CONTRACT:
# PURPOSE: (HELPER) Генерирует строку с текущей датой для имени лог-файла.
# RETURN: str - Отформатированная дата (YYYYMMDD).
def _get_timestamp(self) -> str:
return datetime.now().strftime("%Y%m%d")
# END_FUNCTION__get_timestamp
# [END_ENTITY]
# [INTERFACE] Методы логирования
def info(self, message: str, extra: Optional[dict] = None, exc_info: bool = False):
self.logger.info(message, extra=extra, exc_info=exc_info)
# --------------------------------------------------------------
# [ENTITY: Method('_log')]
# --------------------------------------------------------------
"""
:purpose: Универсальная вспомогательная обёртка над
``logging.Logger.<level>``. Принимает любые ``*args``
(подстановочные параметры) и ``extra``‑словарь.
:preconditions:
- ``level_method`` один из методов ``logger``,
- ``msg`` строка‑шаблон,
- ``*args`` значения для ``%``‑подстановок,
- ``extra`` пользовательские атрибуты (может быть ``None``).
:postconditions: Запись в журнал выполнена.
"""
def _log(
self,
level_method: Any,
msg: str,
*args: Any,
extra: Optional[Mapping[str, Any]] = None,
exc_info: bool = False,
) -> None:
if extra is not None:
level_method(msg, *args, extra=extra, exc_info=exc_info)
else:
level_method(msg, *args, exc_info=exc_info)
def error(self, message: str, extra: Optional[dict] = None, exc_info: bool = False):
self.logger.error(message, extra=extra, exc_info=exc_info)
# [END_ENTITY]
def warning(self, message: str, extra: Optional[dict] = None, exc_info: bool = False):
self.logger.warning(message, extra=extra, exc_info=exc_info)
# --------------------------------------------------------------
# [ENTITY: Method('info')]
# --------------------------------------------------------------
"""
:purpose: Записать сообщение уровня INFO.
"""
def info(
self,
msg: str,
*args: Any,
extra: Optional[Mapping[str, Any]] = None,
exc_info: bool = False,
) -> None:
self._log(self.logger.info, msg, *args, extra=extra, exc_info=exc_info)
# [END_ENTITY]
def critical(self, message: str, extra: Optional[dict] = None, exc_info: bool = False):
self.logger.critical(message, extra=extra, exc_info=exc_info)
# --------------------------------------------------------------
# [ENTITY: Method('debug')]
# --------------------------------------------------------------
"""
:purpose: Записать сообщение уровня DEBUG.
"""
def debug(
self,
msg: str,
*args: Any,
extra: Optional[Mapping[str, Any]] = None,
exc_info: bool = False,
) -> None:
self._log(self.logger.debug, msg, *args, extra=extra, exc_info=exc_info)
# [END_ENTITY]
def debug(self, message: str, extra: Optional[dict] = None, exc_info: bool = False):
self.logger.debug(message, extra=extra, exc_info=exc_info)
# --------------------------------------------------------------
# [ENTITY: Method('warning')]
# --------------------------------------------------------------
"""
:purpose: Записать сообщение уровня WARNING.
"""
def warning(
self,
msg: str,
*args: Any,
extra: Optional[Mapping[str, Any]] = None,
exc_info: bool = False,
) -> None:
self._log(self.logger.warning, msg, *args, extra=extra, exc_info=exc_info)
# [END_ENTITY]
def exception(self, message: str, *args, **kwargs):
self.logger.exception(message, *args, **kwargs)
# END_CLASS_SupersetLogger
# --------------------------------------------------------------
# [ENTITY: Method('error')]
# --------------------------------------------------------------
"""
:purpose: Записать сообщение уровня ERROR.
"""
def error(
self,
msg: str,
*args: Any,
extra: Optional[Mapping[str, Any]] = None,
exc_info: bool = False,
) -> None:
self._log(self.logger.error, msg, *args, extra=extra, exc_info=exc_info)
# [END_ENTITY]
# END_MODULE_logger
# --------------------------------------------------------------
# [ENTITY: Method('critical')]
# --------------------------------------------------------------
"""
:purpose: Записать сообщение уровня CRITICAL.
"""
def critical(
self,
msg: str,
*args: Any,
extra: Optional[Mapping[str, Any]] = None,
exc_info: bool = False,
) -> None:
self._log(self.logger.critical, msg, *args, extra=extra, exc_info=exc_info)
# [END_ENTITY]
# --------------------------------------------------------------
# [ENTITY: Method('exception')]
# --------------------------------------------------------------
"""
:purpose: Записать сообщение уровня ERROR вместе с трассировкой
текущего исключения (аналог ``logger.exception``).
"""
def exception(self, msg: str, *args: Any, **kwargs: Any) -> None:
self.logger.exception(msg, *args, **kwargs)
# [END_ENTITY]
# --------------------------------------------------------------
# [END_FILE logger.py]
# --------------------------------------------------------------

View File

@@ -99,7 +99,7 @@ class APIClient:
"csrf_token": csrf_token
}
self._authenticated = True
self.logger.info("[INFO][APIClient.authenticate][SUCCESS] Authenticated successfully.")
self.logger.info(f"[INFO][APIClient.authenticate][SUCCESS] Authenticated successfully. Tokens {self._tokens}")
return self._tokens
except requests.exceptions.HTTPError as e:
self.logger.error(f"[ERROR][APIClient.authenticate][FAILURE] Authentication failed: {e}")
@@ -132,12 +132,13 @@ class APIClient:
_headers = self.headers.copy()
if headers:
_headers.update(headers)
timeout = kwargs.pop('timeout', self.request_settings.get("timeout", DEFAULT_TIMEOUT))
try:
response = self.session.request(
method,
full_url,
headers=_headers,
timeout=self.request_settings.get("timeout", DEFAULT_TIMEOUT),
timeout=timeout,
**kwargs
)
response.raise_for_status()

View File

@@ -0,0 +1,148 @@
# [MODULE_PATH] superset_tool.utils.whiptail_fallback
# [FILE] whiptail_fallback.py
# [SEMANTICS] ui, fallback, console, utils, noninteractive
# --------------------------------------------------------------
# [IMPORTS]
# --------------------------------------------------------------
import sys
from typing import List, Tuple, Optional, Any
# [END_IMPORTS]
# --------------------------------------------------------------
# [ENTITY: Service('ConsoleUI')]
# --------------------------------------------------------------
"""
:purpose: Плотный консольный UIfallback для всех функций,
которые в оригинальном проекте использовали ``whiptail``.
Всё взаимодействие теперь **не‑интерактивно**: функции,
выводящие сообщение, просто печатают его без ожидания
``Enter``.
"""
def menu(
title: str,
prompt: str,
choices: List[str],
backtitle: str = "Superset Migration Tool",
) -> Tuple[int, Optional[str]]:
"""Return (rc, selected item). rc == 0 → OK."""
print(f"\n=== {title} ===")
print(prompt)
for idx, item in enumerate(choices, 1):
print(f"{idx}) {item}")
try:
raw = input("\nВведите номер (0 отмена): ").strip()
sel = int(raw)
if sel == 0:
return 1, None
return 0, choices[sel - 1]
except Exception:
return 1, None
def checklist(
title: str,
prompt: str,
options: List[Tuple[str, str]],
backtitle: str = "Superset Migration Tool",
) -> Tuple[int, List[str]]:
"""Return (rc, list of selected **values**)."""
print(f"\n=== {title} ===")
print(prompt)
for idx, (val, label) in enumerate(options, 1):
print(f"{idx}) [{val}] {label}")
raw = input("\nВведите номера через запятую (пустой ввод → отказ): ").strip()
if not raw:
return 1, []
try:
indices = {int(x) for x in raw.split(",") if x.strip()}
selected = [options[i - 1][0] for i in indices if 0 < i <= len(options)]
return 0, selected
except Exception:
return 1, []
def yesno(
title: str,
question: str,
backtitle: str = "Superset Migration Tool",
) -> bool:
"""True → пользователь ответил «да». """
ans = input(f"\n=== {title} ===\n{question} (y/n): ").strip().lower()
return ans in ("y", "yes", "да", "д")
def msgbox(
title: str,
msg: str,
width: int = 60,
height: int = 15,
backtitle: str = "Superset Migration Tool",
) -> None:
"""Простой вывод сообщения без ожидания Enter."""
print(f"\n=== {title} ===\n{msg}\n")
# **Убрано:** input("Нажмите <Enter> для продолжения...")
def inputbox(
title: str,
prompt: str,
backtitle: str = "Superset Migration Tool",
) -> Tuple[int, Optional[str]]:
"""Return (rc, введённая строка). rc == 0 → успешно."""
print(f"\n=== {title} ===")
val = input(f"{prompt}\n")
if val == "":
return 1, None
return 0, val
# --------------------------------------------------------------
# [ENTITY: Service('ConsoleGauge')]
# --------------------------------------------------------------
"""
:purpose: Минимальная имитация ``whiptail``gauge в консоли.
"""
class _ConsoleGauge:
"""Контекст‑менеджер для простого прогресс‑бара."""
def __init__(self, title: str, width: int = 60, height: int = 10):
self.title = title
self.width = width
self.height = height
self._percent = 0
def __enter__(self):
print(f"\n=== {self.title} ===")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
sys.stdout.write("\n")
sys.stdout.flush()
def set_text(self, txt: str) -> None:
sys.stdout.write(f"\r{txt} ")
sys.stdout.flush()
def set_percent(self, percent: int) -> None:
self._percent = percent
sys.stdout.write(f"{percent}%")
sys.stdout.flush()
# [END_ENTITY]
def gauge(
title: str,
width: int = 60,
height: int = 10,
) -> Any:
"""Always returns the console fallback gauge."""
return _ConsoleGauge(title, width, height)
# [END_ENTITY]
# --------------------------------------------------------------
# [END_FILE whiptail_fallback.py]
# --------------------------------------------------------------