refactor, add db search
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
# <GRACE_MODULE id="superset_tool.utils.fileio" name="fileio.py">
|
||||
# @SEMANTICS: file, io, zip, yaml, temp, archive, utility
|
||||
# @PURPOSE: Предоставляет набор утилит для управления файловыми операциями, включая работу с временными файлами, архивами ZIP, файлами YAML и очистку директорий.
|
||||
# @DEPENDS_ON: superset_tool.exceptions -> Для генерации специфичных ошибок.
|
||||
# @DEPENDS_ON: superset_tool.utils.logger -> Для логирования операций.
|
||||
# @DEPENDS_ON: pyyaml -> Для работы с YAML файлами.
|
||||
# [DEF:superset_tool.utils.fileio:Module]
|
||||
#
|
||||
# @SEMANTICS: file, io, zip, yaml, temp, archive, utility
|
||||
# @PURPOSE: Предоставляет набор утилит для управления файловыми операциями, включая работу с временными файлами, архивами ZIP, файлами YAML и очистку директорий.
|
||||
# @LAYER: Infra
|
||||
# @RELATION: DEPENDS_ON -> superset_tool.exceptions
|
||||
# @RELATION: DEPENDS_ON -> superset_tool.utils.logger
|
||||
# @RELATION: DEPENDS_ON -> pyyaml
|
||||
# @PUBLIC_API: create_temp_file, remove_empty_directories, read_dashboard_from_disk, calculate_crc32, RetentionPolicy, archive_exports, save_and_unpack_dashboard, update_yamls, create_dashboard_export, sanitize_filename, get_filename_from_headers, consolidate_archive_folders
|
||||
|
||||
# <IMPORTS>
|
||||
# [SECTION: IMPORTS]
|
||||
import os
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Tuple, Dict, List, Union, LiteralString
|
||||
from typing import Any, Optional, Tuple, Dict, List, Union, LiteralString, Generator
|
||||
from contextlib import contextmanager
|
||||
import tempfile
|
||||
from datetime import date, datetime
|
||||
@@ -21,20 +24,18 @@ from dataclasses import dataclass
|
||||
import yaml
|
||||
from superset_tool.exceptions import InvalidZipFormatError
|
||||
from superset_tool.utils.logger import SupersetLogger
|
||||
# </IMPORTS>
|
||||
# [/SECTION]
|
||||
|
||||
# --- Начало кода модуля ---
|
||||
|
||||
# <ANCHOR id="create_temp_file" type="Function">
|
||||
# @PURPOSE: Контекстный менеджер для создания временного файла или директории с гарантированным удалением.
|
||||
# @PARAM: content: Optional[bytes] - Бинарное содержимое для записи во временный файл.
|
||||
# @PARAM: suffix: str - Суффикс ресурса. Если `.dir`, создается директория.
|
||||
# @PARAM: mode: str - Режим записи в файл (e.g., 'wb').
|
||||
# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
|
||||
# @YIELDS: Path - Путь к временному ресурсу.
|
||||
# @THROW: IOError - При ошибках создания ресурса.
|
||||
# [DEF:create_temp_file:Function]
|
||||
# @PURPOSE: Контекстный менеджер для создания временного файла или директории с гарантированным удалением.
|
||||
# @PARAM: content (Optional[bytes]) - Бинарное содержимое для записи во временный файл.
|
||||
# @PARAM: suffix (str) - Суффикс ресурса. Если `.dir`, создается директория.
|
||||
# @PARAM: mode (str) - Режим записи в файл (e.g., 'wb').
|
||||
# @PARAM: logger (Optional[SupersetLogger]) - Экземпляр логгера.
|
||||
# @YIELDS: Path - Путь к временному ресурсу.
|
||||
# @THROW: IOError - При ошибках создания ресурса.
|
||||
@contextmanager
|
||||
def create_temp_file(content: Optional[bytes] = None, suffix: str = ".zip", mode: str = 'wb', logger: Optional[SupersetLogger] = None) -> Path:
|
||||
def create_temp_file(content: Optional[bytes] = None, suffix: str = ".zip", mode: str = 'wb', logger: Optional[SupersetLogger] = None) -> Generator[Path, None, None]:
|
||||
logger = logger or SupersetLogger(name="fileio")
|
||||
resource_path = None
|
||||
is_dir = suffix.startswith('.dir')
|
||||
@@ -63,13 +64,13 @@ def create_temp_file(content: Optional[bytes] = None, suffix: str = ".zip", mode
|
||||
logger.debug("[create_temp_file][Cleanup] Removed temporary file: %s", resource_path)
|
||||
except OSError as e:
|
||||
logger.error("[create_temp_file][Failure] Error during cleanup of %s: %s", resource_path, e)
|
||||
# </ANCHOR id="create_temp_file">
|
||||
# [/DEF:create_temp_file]
|
||||
|
||||
# <ANCHOR id="remove_empty_directories" type="Function">
|
||||
# @PURPOSE: Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути.
|
||||
# @PARAM: root_dir: str - Путь к корневой директории для очистки.
|
||||
# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
|
||||
# @RETURN: int - Количество удаленных директорий.
|
||||
# [DEF:remove_empty_directories:Function]
|
||||
# @PURPOSE: Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути.
|
||||
# @PARAM: root_dir (str) - Путь к корневой директории для очистки.
|
||||
# @PARAM: logger (Optional[SupersetLogger]) - Экземпляр логгера.
|
||||
# @RETURN: int - Количество удаленных директорий.
|
||||
def remove_empty_directories(root_dir: str, logger: Optional[SupersetLogger] = None) -> int:
|
||||
logger = logger or SupersetLogger(name="fileio")
|
||||
logger.info("[remove_empty_directories][Enter] Starting cleanup of empty directories in %s", root_dir)
|
||||
@@ -87,14 +88,14 @@ def remove_empty_directories(root_dir: str, logger: Optional[SupersetLogger] = N
|
||||
logger.error("[remove_empty_directories][Failure] Failed to remove %s: %s", current_dir, e)
|
||||
logger.info("[remove_empty_directories][Exit] Removed %d empty directories.", removed_count)
|
||||
return removed_count
|
||||
# </ANCHOR id="remove_empty_directories">
|
||||
# [/DEF:remove_empty_directories]
|
||||
|
||||
# <ANCHOR id="read_dashboard_from_disk" type="Function">
|
||||
# @PURPOSE: Читает бинарное содержимое файла с диска.
|
||||
# @PARAM: file_path: str - Путь к файлу.
|
||||
# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
|
||||
# @RETURN: Tuple[bytes, str] - Кортеж (содержимое, имя файла).
|
||||
# @THROW: FileNotFoundError - Если файл не найден.
|
||||
# [DEF:read_dashboard_from_disk:Function]
|
||||
# @PURPOSE: Читает бинарное содержимое файла с диска.
|
||||
# @PARAM: file_path (str) - Путь к файлу.
|
||||
# @PARAM: logger (Optional[SupersetLogger]) - Экземпляр логгера.
|
||||
# @RETURN: Tuple[bytes, str] - Кортеж (содержимое, имя файла).
|
||||
# @THROW: FileNotFoundError - Если файл не найден.
|
||||
def read_dashboard_from_disk(file_path: str, logger: Optional[SupersetLogger] = None) -> Tuple[bytes, str]:
|
||||
logger = logger or SupersetLogger(name="fileio")
|
||||
path = Path(file_path)
|
||||
@@ -104,36 +105,36 @@ def read_dashboard_from_disk(file_path: str, logger: Optional[SupersetLogger] =
|
||||
if not content:
|
||||
logger.warning("[read_dashboard_from_disk][Warning] File is empty: %s", file_path)
|
||||
return content, path.name
|
||||
# </ANCHOR id="read_dashboard_from_disk">
|
||||
# [/DEF:read_dashboard_from_disk]
|
||||
|
||||
# <ANCHOR id="calculate_crc32" type="Function">
|
||||
# @PURPOSE: Вычисляет контрольную сумму CRC32 для файла.
|
||||
# @PARAM: file_path: Path - Путь к файлу.
|
||||
# @RETURN: str - 8-значное шестнадцатеричное представление CRC32.
|
||||
# @THROW: IOError - При ошибках чтения файла.
|
||||
# [DEF:calculate_crc32:Function]
|
||||
# @PURPOSE: Вычисляет контрольную сумму CRC32 для файла.
|
||||
# @PARAM: file_path (Path) - Путь к файлу.
|
||||
# @RETURN: str - 8-значное шестнадцатеричное представление CRC32.
|
||||
# @THROW: IOError - При ошибках чтения файла.
|
||||
def calculate_crc32(file_path: Path) -> str:
|
||||
with open(file_path, 'rb') as f:
|
||||
crc32_value = zlib.crc32(f.read())
|
||||
return f"{crc32_value:08x}"
|
||||
# </ANCHOR id="calculate_crc32">
|
||||
# [/DEF:calculate_crc32]
|
||||
|
||||
# <ANCHOR id="RetentionPolicy" type="DataClass">
|
||||
# @PURPOSE: Определяет политику хранения для архивов (ежедневные, еженедельные, ежемесячные).
|
||||
# [DEF:RetentionPolicy:DataClass]
|
||||
# @PURPOSE: Определяет политику хранения для архивов (ежедневные, еженедельные, ежемесячные).
|
||||
@dataclass
|
||||
class RetentionPolicy:
|
||||
daily: int = 7
|
||||
weekly: int = 4
|
||||
monthly: int = 12
|
||||
# </ANCHOR id="RetentionPolicy">
|
||||
# [/DEF:RetentionPolicy]
|
||||
|
||||
# <ANCHOR id="archive_exports" type="Function">
|
||||
# @PURPOSE: Управляет архивом экспортированных файлов, применяя политику хранения и дедупликацию.
|
||||
# @PARAM: output_dir: str - Директория с архивами.
|
||||
# @PARAM: policy: RetentionPolicy - Политика хранения.
|
||||
# @PARAM: deduplicate: bool - Флаг для включения удаления дубликатов по CRC32.
|
||||
# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
|
||||
# @RELATION: CALLS -> apply_retention_policy
|
||||
# @RELATION: CALLS -> calculate_crc32
|
||||
# [DEF:archive_exports:Function]
|
||||
# @PURPOSE: Управляет архивом экспортированных файлов, применяя политику хранения и дедупликацию.
|
||||
# @RELATION: CALLS -> apply_retention_policy
|
||||
# @RELATION: CALLS -> calculate_crc32
|
||||
# @PARAM: output_dir (str) - Директория с архивами.
|
||||
# @PARAM: policy (RetentionPolicy) - Политика хранения.
|
||||
# @PARAM: deduplicate (bool) - Флаг для включения удаления дубликатов по CRC32.
|
||||
# @PARAM: logger (Optional[SupersetLogger]) - Экземпляр логгера.
|
||||
def archive_exports(output_dir: str, policy: RetentionPolicy, deduplicate: bool = False, logger: Optional[SupersetLogger] = None) -> None:
|
||||
logger = logger or SupersetLogger(name="fileio")
|
||||
output_path = Path(output_dir)
|
||||
@@ -142,16 +143,78 @@ def archive_exports(output_dir: str, policy: RetentionPolicy, deduplicate: bool
|
||||
return
|
||||
|
||||
logger.info("[archive_exports][Enter] Managing archive in %s", output_dir)
|
||||
# ... (логика дедупликации и политики хранения) ...
|
||||
# </ANCHOR id="archive_exports">
|
||||
|
||||
# 1. Collect all zip files
|
||||
zip_files = list(output_path.glob("*.zip"))
|
||||
if not zip_files:
|
||||
logger.info("[archive_exports][State] No zip files found in %s", output_dir)
|
||||
return
|
||||
|
||||
# <ANCHOR id="apply_retention_policy" type="Function">
|
||||
# @PURPOSE: (Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить.
|
||||
# @INTERNAL
|
||||
# @PARAM: files_with_dates: List[Tuple[Path, date]] - Список файлов с датами.
|
||||
# @PARAM: policy: RetentionPolicy - Политика хранения.
|
||||
# @PARAM: logger: SupersetLogger - Логгер.
|
||||
# @RETURN: set - Множество путей к файлам, которые должны быть сохранены.
|
||||
# 2. Deduplication
|
||||
if deduplicate:
|
||||
logger.info("[archive_exports][State] Starting deduplication...")
|
||||
checksums = {}
|
||||
files_to_remove = []
|
||||
|
||||
# Sort by modification time (newest first) to keep the latest version
|
||||
zip_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
|
||||
|
||||
for file_path in zip_files:
|
||||
try:
|
||||
crc = calculate_crc32(file_path)
|
||||
if crc in checksums:
|
||||
files_to_remove.append(file_path)
|
||||
logger.debug("[archive_exports][State] Duplicate found: %s (same as %s)", file_path.name, checksums[crc].name)
|
||||
else:
|
||||
checksums[crc] = file_path
|
||||
except Exception as e:
|
||||
logger.error("[archive_exports][Failure] Failed to calculate CRC32 for %s: %s", file_path, e)
|
||||
|
||||
for f in files_to_remove:
|
||||
try:
|
||||
f.unlink()
|
||||
zip_files.remove(f)
|
||||
logger.info("[archive_exports][State] Removed duplicate: %s", f.name)
|
||||
except OSError as e:
|
||||
logger.error("[archive_exports][Failure] Failed to remove duplicate %s: %s", f, e)
|
||||
|
||||
# 3. Retention Policy
|
||||
files_with_dates = []
|
||||
for file_path in zip_files:
|
||||
# Try to extract date from filename
|
||||
# Pattern: ..._YYYYMMDD_HHMMSS.zip or ..._YYYYMMDD.zip
|
||||
match = re.search(r'_(\d{8})_', file_path.name)
|
||||
file_date = None
|
||||
if match:
|
||||
try:
|
||||
date_str = match.group(1)
|
||||
file_date = datetime.strptime(date_str, "%Y%m%d").date()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not file_date:
|
||||
# Fallback to modification time
|
||||
file_date = datetime.fromtimestamp(file_path.stat().st_mtime).date()
|
||||
|
||||
files_with_dates.append((file_path, file_date))
|
||||
|
||||
files_to_keep = apply_retention_policy(files_with_dates, policy, logger)
|
||||
|
||||
for file_path, _ in files_with_dates:
|
||||
if file_path not in files_to_keep:
|
||||
try:
|
||||
file_path.unlink()
|
||||
logger.info("[archive_exports][State] Removed by retention policy: %s", file_path.name)
|
||||
except OSError as e:
|
||||
logger.error("[archive_exports][Failure] Failed to remove %s: %s", file_path, e)
|
||||
# [/DEF:archive_exports]
|
||||
|
||||
# [DEF:apply_retention_policy:Function]
|
||||
# @PURPOSE: (Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить.
|
||||
# @PARAM: files_with_dates (List[Tuple[Path, date]]) - Список файлов с датами.
|
||||
# @PARAM: policy (RetentionPolicy) - Политика хранения.
|
||||
# @PARAM: logger (SupersetLogger) - Логгер.
|
||||
# @RETURN: set - Множество путей к файлам, которые должны быть сохранены.
|
||||
def apply_retention_policy(files_with_dates: List[Tuple[Path, date]], policy: RetentionPolicy, logger: SupersetLogger) -> set:
|
||||
# Сортируем по дате (от новой к старой)
|
||||
sorted_files = sorted(files_with_dates, key=lambda x: x[1], reverse=True)
|
||||
@@ -177,17 +240,17 @@ def apply_retention_policy(files_with_dates: List[Tuple[Path, date]], policy: Re
|
||||
files_to_keep.update(monthly_files[:policy.monthly])
|
||||
logger.debug("[apply_retention_policy][State] Keeping %d files according to retention policy", len(files_to_keep))
|
||||
return files_to_keep
|
||||
# </ANCHOR id="apply_retention_policy">
|
||||
# [/DEF:apply_retention_policy]
|
||||
|
||||
# <ANCHOR id="save_and_unpack_dashboard" type="Function">
|
||||
# @PURPOSE: Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его.
|
||||
# @PARAM: zip_content: bytes - Содержимое ZIP-архива.
|
||||
# @PARAM: output_dir: Union[str, Path] - Директория для сохранения.
|
||||
# @PARAM: unpack: bool - Флаг, нужно ли распаковывать архив.
|
||||
# @PARAM: original_filename: Optional[str] - Исходное имя файла для сохранения.
|
||||
# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
|
||||
# @RETURN: Tuple[Path, Optional[Path]] - Путь к ZIP-файлу и, если применимо, путь к директории с распаковкой.
|
||||
# @THROW: InvalidZipFormatError - При ошибке формата ZIP.
|
||||
# [DEF:save_and_unpack_dashboard:Function]
|
||||
# @PURPOSE: Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его.
|
||||
# @PARAM: zip_content (bytes) - Содержимое ZIP-архива.
|
||||
# @PARAM: output_dir (Union[str, Path]) - Директория для сохранения.
|
||||
# @PARAM: unpack (bool) - Флаг, нужно ли распаковывать архив.
|
||||
# @PARAM: original_filename (Optional[str]) - Исходное имя файла для сохранения.
|
||||
# @PARAM: logger (Optional[SupersetLogger]) - Экземпляр логгера.
|
||||
# @RETURN: Tuple[Path, Optional[Path]] - Путь к ZIP-файлу и, если применимо, путь к директории с распаковкой.
|
||||
# @THROW: InvalidZipFormatError - При ошибке формата ZIP.
|
||||
def save_and_unpack_dashboard(zip_content: bytes, output_dir: Union[str, Path], unpack: bool = False, original_filename: Optional[str] = None, logger: Optional[SupersetLogger] = None) -> Tuple[Path, Optional[Path]]:
|
||||
logger = logger or SupersetLogger(name="fileio")
|
||||
logger.info("[save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: %s", unpack)
|
||||
@@ -207,17 +270,17 @@ def save_and_unpack_dashboard(zip_content: bytes, output_dir: Union[str, Path],
|
||||
except zipfile.BadZipFile as e:
|
||||
logger.error("[save_and_unpack_dashboard][Failure] Invalid ZIP archive: %s", e)
|
||||
raise InvalidZipFormatError(f"Invalid ZIP file: {e}") from e
|
||||
# </ANCHOR id="save_and_unpack_dashboard">
|
||||
# [/DEF:save_and_unpack_dashboard]
|
||||
|
||||
# <ANCHOR id="update_yamls" type="Function">
|
||||
# @PURPOSE: Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex.
|
||||
# @PARAM: db_configs: Optional[List[Dict]] - Список конфигураций для замены.
|
||||
# @PARAM: path: str - Путь к директории с YAML файлами.
|
||||
# @PARAM: regexp_pattern: Optional[LiteralString] - Паттерн для поиска.
|
||||
# @PARAM: replace_string: Optional[LiteralString] - Строка для замены.
|
||||
# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
|
||||
# @THROW: FileNotFoundError - Если `path` не существует.
|
||||
# @RELATION: CALLS -> _update_yaml_file
|
||||
# [DEF:update_yamls:Function]
|
||||
# @PURPOSE: Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex.
|
||||
# @RELATION: CALLS -> _update_yaml_file
|
||||
# @THROW: FileNotFoundError - Если `path` не существует.
|
||||
# @PARAM: db_configs (Optional[List[Dict]]) - Список конфигураций для замены.
|
||||
# @PARAM: path (str) - Путь к директории с YAML файлами.
|
||||
# @PARAM: regexp_pattern (Optional[LiteralString]) - Паттерн для поиска.
|
||||
# @PARAM: replace_string (Optional[LiteralString]) - Строка для замены.
|
||||
# @PARAM: logger (Optional[SupersetLogger]) - Экземпляр логгера.
|
||||
def update_yamls(db_configs: Optional[List[Dict]] = None, path: str = "dashboards", regexp_pattern: Optional[LiteralString] = None, replace_string: Optional[LiteralString] = None, logger: Optional[SupersetLogger] = None) -> None:
|
||||
logger = logger or SupersetLogger(name="fileio")
|
||||
logger.info("[update_yamls][Enter] Starting YAML configuration update.")
|
||||
@@ -228,57 +291,64 @@ def update_yamls(db_configs: Optional[List[Dict]] = None, path: str = "dashboard
|
||||
|
||||
for file_path in dir_path.rglob("*.yaml"):
|
||||
_update_yaml_file(file_path, configs, regexp_pattern, replace_string, logger)
|
||||
# </ANCHOR id="update_yamls">
|
||||
# [/DEF:update_yamls]
|
||||
|
||||
# <ANCHOR id="_update_yaml_file" type="Function">
|
||||
# @PURPOSE: (Helper) Обновляет один YAML файл.
|
||||
# @INTERNAL
|
||||
def _update_yaml_file(file_path: Path, db_configs: List[Dict], regexp_pattern: Optional[str], replace_string: Optional[str], logger: SupersetLogger) -> None:
|
||||
# Читаем содержимое файла
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
logger.error("[_update_yaml_file][Failure] Failed to read %s: %s", file_path, e)
|
||||
return
|
||||
# Если задан pattern и replace_string, применяем замену по регулярному выражению
|
||||
if regexp_pattern and replace_string:
|
||||
try:
|
||||
new_content = re.sub(regexp_pattern, replace_string, content)
|
||||
if new_content != content:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
logger.info("[_update_yaml_file][State] Updated %s using regex pattern", file_path)
|
||||
except Exception as e:
|
||||
logger.error("[_update_yaml_file][Failure] Error applying regex to %s: %s", file_path, e)
|
||||
# Если заданы конфигурации, заменяем значения
|
||||
if db_configs:
|
||||
try:
|
||||
parsed_data = yaml.safe_load(content)
|
||||
if not isinstance(parsed_data, dict):
|
||||
logger.warning("[_update_yaml_file][Warning] YAML content is not a dictionary in %s", file_path)
|
||||
return
|
||||
# Обновляем данные
|
||||
for config in db_configs:
|
||||
for key, value in config.items():
|
||||
if key in parsed_data:
|
||||
old_value = parsed_data[key]
|
||||
parsed_data[key] = value
|
||||
logger.info("[_update_yaml_file][State] Changed %s.%s from %s to %s", file_path, key, old_value, value)
|
||||
# Записываем обратно
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(parsed_data, f, default_flow_style=False, allow_unicode=True)
|
||||
except Exception as e:
|
||||
logger.error("[_update_yaml_file][Failure] Error updating YAML in %s: %s", file_path, e)
|
||||
# </ANCHOR id="_update_yaml_file">
|
||||
# [DEF:_update_yaml_file:Function]
|
||||
# @PURPOSE: (Helper) Обновляет один YAML файл.
|
||||
# @PARAM: file_path (Path) - Путь к файлу.
|
||||
# @PARAM: db_configs (List[Dict]) - Конфигурации.
|
||||
# @PARAM: regexp_pattern (Optional[str]) - Паттерн.
|
||||
# @PARAM: replace_string (Optional[str]) - Замена.
|
||||
# @PARAM: logger (SupersetLogger) - Логгер.
|
||||
def _update_yaml_file(file_path: Path, db_configs: List[Dict], regexp_pattern: Optional[str], replace_string: Optional[str], logger: SupersetLogger) -> None:
|
||||
# Читаем содержимое файла
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
logger.error("[_update_yaml_file][Failure] Failed to read %s: %s", file_path, e)
|
||||
return
|
||||
# Если задан pattern и replace_string, применяем замену по регулярному выражению
|
||||
if regexp_pattern and replace_string:
|
||||
try:
|
||||
new_content = re.sub(regexp_pattern, replace_string, content)
|
||||
if new_content != content:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
logger.info("[_update_yaml_file][State] Updated %s using regex pattern", file_path)
|
||||
except Exception as e:
|
||||
logger.error("[_update_yaml_file][Failure] Error applying regex to %s: %s", file_path, e)
|
||||
# Если заданы конфигурации, заменяем значения (поддержка old/new)
|
||||
if db_configs:
|
||||
try:
|
||||
# Прямой текстовый заменитель для старых/новых значений, чтобы сохранить структуру файла
|
||||
modified_content = content
|
||||
for cfg in db_configs:
|
||||
# Ожидаем структуру: {'old': {...}, 'new': {...}}
|
||||
old_cfg = cfg.get('old', {})
|
||||
new_cfg = cfg.get('new', {})
|
||||
for key, old_val in old_cfg.items():
|
||||
if key in new_cfg:
|
||||
new_val = new_cfg[key]
|
||||
# Заменяем только точные совпадения старого значения в тексте YAML
|
||||
if isinstance(old_val, str):
|
||||
escaped_old = re.escape(old_val)
|
||||
modified_content = re.sub(escaped_old, new_val, modified_content)
|
||||
logger.info("[_update_yaml_file][State] Replaced '%s' with '%s' for key %s in %s", old_val, new_val, key, file_path)
|
||||
# Записываем обратно изменённый контент без парсинга YAML, сохраняем оригинальное форматирование
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(modified_content)
|
||||
except Exception as e:
|
||||
logger.error("[_update_yaml_file][Failure] Error performing raw replacement in %s: %s", file_path, e)
|
||||
# [/DEF:_update_yaml_file]
|
||||
|
||||
# <ANCHOR id="create_dashboard_export" type="Function">
|
||||
# @PURPOSE: Создает ZIP-архив из указанных исходных путей.
|
||||
# @PARAM: zip_path: Union[str, Path] - Путь для сохранения ZIP архива.
|
||||
# @PARAM: source_paths: List[Union[str, Path]] - Список исходных путей для архивации.
|
||||
# @PARAM: exclude_extensions: Optional[List[str]] - Список расширений для исключения.
|
||||
# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
|
||||
# @RETURN: bool - `True` при успехе, `False` при ошибке.
|
||||
# [DEF:create_dashboard_export:Function]
|
||||
# @PURPOSE: Создает ZIP-архив из указанных исходных путей.
|
||||
# @PARAM: zip_path (Union[str, Path]) - Путь для сохранения ZIP архива.
|
||||
# @PARAM: source_paths (List[Union[str, Path]]) - Список исходных путей для архивации.
|
||||
# @PARAM: exclude_extensions (Optional[List[str]]) - Список расширений для исключения.
|
||||
# @PARAM: logger (Optional[SupersetLogger]) - Экземпляр логгера.
|
||||
# @RETURN: bool - `True` при успехе, `False` при ошибке.
|
||||
def create_dashboard_export(zip_path: Union[str, Path], source_paths: List[Union[str, Path]], exclude_extensions: Optional[List[str]] = None, logger: Optional[SupersetLogger] = None) -> bool:
|
||||
logger = logger or SupersetLogger(name="fileio")
|
||||
logger.info("[create_dashboard_export][Enter] Packing dashboard: %s -> %s", source_paths, zip_path)
|
||||
@@ -297,32 +367,32 @@ def create_dashboard_export(zip_path: Union[str, Path], source_paths: List[Union
|
||||
except (IOError, zipfile.BadZipFile, AssertionError) as e:
|
||||
logger.error("[create_dashboard_export][Failure] Error: %s", e, exc_info=True)
|
||||
return False
|
||||
# </ANCHOR id="create_dashboard_export">
|
||||
# [/DEF:create_dashboard_export]
|
||||
|
||||
# <ANCHOR id="sanitize_filename" type="Function">
|
||||
# @PURPOSE: Очищает строку от символов, недопустимых в именах файлов.
|
||||
# @PARAM: filename: str - Исходное имя файла.
|
||||
# @RETURN: str - Очищенная строка.
|
||||
# [DEF:sanitize_filename:Function]
|
||||
# @PURPOSE: Очищает строку от символов, недопустимых в именах файлов.
|
||||
# @PARAM: filename (str) - Исходное имя файла.
|
||||
# @RETURN: str - Очищенная строка.
|
||||
def sanitize_filename(filename: str) -> str:
|
||||
return re.sub(r'[\\/*?:"<>|]', "_", filename).strip()
|
||||
# </ANCHOR id="sanitize_filename">
|
||||
# [/DEF:sanitize_filename]
|
||||
|
||||
# <ANCHOR id="get_filename_from_headers" type="Function">
|
||||
# @PURPOSE: Извлекает имя файла из HTTP заголовка 'Content-Disposition'.
|
||||
# @PARAM: headers: dict - Словарь HTTP заголовков.
|
||||
# @RETURN: Optional[str] - Имя файла или `None`.
|
||||
# [DEF:get_filename_from_headers:Function]
|
||||
# @PURPOSE: Извлекает имя файла из HTTP заголовка 'Content-Disposition'.
|
||||
# @PARAM: headers (dict) - Словарь HTTP заголовков.
|
||||
# @RETURN: Optional[str] - Имя файла или `None`.
|
||||
def get_filename_from_headers(headers: dict) -> Optional[str]:
|
||||
content_disposition = headers.get("Content-Disposition", "")
|
||||
if match := re.search(r'filename="?([^"]+)"?', content_disposition):
|
||||
return match.group(1).strip()
|
||||
return None
|
||||
# </ANCHOR id="get_filename_from_headers">
|
||||
# [/DEF:get_filename_from_headers]
|
||||
|
||||
# <ANCHOR id="consolidate_archive_folders" type="Function">
|
||||
# @PURPOSE: Консолидирует директории архивов на основе общего слага в имени.
|
||||
# @PARAM: root_directory: Path - Корневая директория для консолидации.
|
||||
# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
|
||||
# @THROW: TypeError, ValueError - Если `root_directory` невалиден.
|
||||
# [DEF:consolidate_archive_folders:Function]
|
||||
# @PURPOSE: Консолидирует директории архивов на основе общего слага в имени.
|
||||
# @THROW: TypeError, ValueError - Если `root_directory` невалиден.
|
||||
# @PARAM: root_directory (Path) - Корневая директория для консолидации.
|
||||
# @PARAM: logger (Optional[SupersetLogger]) - Экземпляр логгера.
|
||||
def consolidate_archive_folders(root_directory: Path, logger: Optional[SupersetLogger] = None) -> None:
|
||||
logger = logger or SupersetLogger(name="fileio")
|
||||
assert isinstance(root_directory, Path), "root_directory must be a Path object."
|
||||
@@ -371,8 +441,6 @@ def consolidate_archive_folders(root_directory: Path, logger: Optional[SupersetL
|
||||
logger.info("[consolidate_archive_folders][State] Removed source directory: %s", source_dir)
|
||||
except Exception as e:
|
||||
logger.error("[consolidate_archive_folders][Failure] Failed to remove source directory %s: %s", source_dir, e)
|
||||
# </ANCHOR id="consolidate_archive_folders">
|
||||
# [/DEF:consolidate_archive_folders]
|
||||
|
||||
# --- Конец кода модуля ---
|
||||
|
||||
# </GRACE_MODULE id="superset_tool.utils.fileio">
|
||||
# [/DEF:superset_tool.utils.fileio]
|
||||
|
||||
Reference in New Issue
Block a user