diff --git a/.gitignore b/.gitignore index f2746a5..36d39bb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ dashboards/ *.txt *.zip keyring passwords.py -Logs/ \ No newline at end of file +Logs/ +patch1.patch +test postman.py diff --git a/superset_tool/client.py b/superset_tool/client.py index 47ba09a..8f91c6f 100644 --- a/superset_tool/client.py +++ b/superset_tool/client.py @@ -9,7 +9,6 @@ from .exceptions import * from .models import SupersetConfig from .utils.logger import SupersetLogger - class SupersetClient: def __init__(self, config: SupersetConfig): self.config = config @@ -238,22 +237,14 @@ class SupersetClient: raise SupersetAPIError(f"Export failed: {str(e)}") from e def import_dashboard(self, zip_path) -> Dict: - """Импортирует дашборд в Superset из ZIP-архива. + """Импортирует дашборд в Superset из ZIP-архива с детальной обработкой ошибок. Параметры: - zip_path (Path): Путь к ZIP-файлу с дашбордом для импорта + zip_path (Union[str, Path]): Путь к ZIP-файлу с дашбордом Возвращает: dict: Ответ API в формате JSON с результатами импорта - Исключения: - RuntimeError: Вызывается при: - - Ошибках сети/соединения - - Невалидном формате ZIP-архива - - Конфликте прав доступа - - Ошибках сервера (status code >= 400) - - Попытке перезаписи без соответствующих прав - Пример использования: result = client.import_dashboard(Path("my_dashboard.zip")) print(f"Импортирован дашборд: {result['title']}") @@ -269,36 +260,76 @@ class SupersetClient: """ url = f"{self.config.base_url}/dashboard/import/" self.logger.debug(f"Импортируем дашборд ID {zip_path} на {url}...") - headers_without_content_type = { - k: v for k, v in self.headers.items() if k.lower() != 'content-type'} - - zip_name = zip_path.name - # Подготавливаем данные для multipart/form-data - with open(zip_path, 'rb') as f: - files = { - 'formData': ( - zip_name, # Имя файла - f, # Файловый объект - 'application/x-zip-compressed' # MIME-тип из curl - ) - } - - # Отправляем запрос - response = self.session.post( - url, - files=files, - data={'overwrite': 'true'}, - headers=headers_without_content_type, - timeout=self.config.timeout * 2 # Longer timeout for imports - ) - # Обрабатываем ответ + + # Валидация входного файла try: - response.raise_for_status() - self.logger.info(f"Дашборд импортирован успешно") - return response.json() - except requests.exceptions.HTTPError as e: - self.logger.error(f"Ошибка при импорте: {str(e)}", exc_info=True) - error_detail = f"{e.response.status_code} {e.response.reason}" - if e.response.text: - error_detail += f"\nТело ответа: {e.response.text}" - raise RuntimeError(f"Ошибка импорта: {error_detail}") from e + if not Path(zip_path).exists(): + raise FileNotFoundError(f"Файл не найден: {zip_path}") + + if not zipfile.is_zipfile(zip_path): + raise InvalidZipFormatError(f"Файл не является ZIP-архивом: {zip_path}") + + # Дополнительная проверка содержимого архива + with zipfile.ZipFile(zip_path) as zf: + if not any(name.endswith('metadata.yaml') for name in zf.namelist()): + raise DashboardNotFoundError("Архив не содержит metadata.yaml") + + except (FileNotFoundError, InvalidZipFormatError, DashboardNotFoundError) as e: + self.logger.error(f"Ошибка валидации архива: {str(e)}", exc_info=True) + raise + + headers = { + k: v for k, v in self.headers.items() + if k.lower() != 'content-type' + } + + try: + with open(zip_path, 'rb') as f: + files = { + 'formData': ( + Path(zip_path).name, + f, + 'application/x-zip-compressed' + ) + } + + response = self.session.post( + url, + files=files, + data={'overwrite': 'true'}, + headers=headers, + timeout=self.config.timeout * 2 + ) + + # Обработка HTTP-ошибок + if response.status_code == 404: + raise DashboardNotFoundError("Эндпоинт импорта не найден") + elif response.status_code == 403: + raise PermissionDeniedError("Недостаточно прав для импорта") + elif response.status_code >= 500: + raise SupersetServerError(f"Ошибка сервера: {response.status_code}") + + response.raise_for_status() + + self.logger.info(f"Дашборд успешно импортирован из {zip_path}") + return response.json() + + except requests.exceptions.ConnectionError as e: + error_msg = f"Ошибка соединения: {str(e)}" + self.logger.error(error_msg, exc_info=True) + raise NetworkError(error_msg) from e + + except requests.exceptions.Timeout as e: + error_msg = f"Таймаут при импорте дашборда" + self.logger.error(error_msg, exc_info=True) + raise NetworkError(error_msg) from e + + except requests.exceptions.RequestException as e: + error_msg = f"Ошибка при импорте: {str(e)}" + self.logger.error(error_msg, exc_info=True) + raise DashboardImportError(error_msg) from e + + except Exception as e: + error_msg = f"Неожиданная ошибка: {str(e)}" + self.logger.critical(error_msg, exc_info=True) + raise DashboardImportError(error_msg) from e diff --git a/superset_tool/exceptions.py b/superset_tool/exceptions.py index 9b5b22e..18e0d76 100644 --- a/superset_tool/exceptions.py +++ b/superset_tool/exceptions.py @@ -11,4 +11,22 @@ class ExportError(SupersetToolError): """Dashboard export errors""" class ImportError(SupersetToolError): - """Dashboard import errors""" \ No newline at end of file + """Dashboard import errors""" + +class InvalidZipFormatError(SupersetToolError): + "Archive zip errors" + +class DashboardNotFoundError(SupersetToolError): + "404 error" + +class PermissionDeniedError(SupersetToolError): + "403 error" + +class SupersetServerError(SupersetToolError): + "500 error" + +class NetworkError(SupersetToolError): + "Network errors" + +class DashboardImportError(SupersetToolError): + "Api import errors" \ No newline at end of file diff --git a/superset_tool/utils/fileio.py b/superset_tool/utils/fileio.py index 2e0452d..4143a1a 100644 --- a/superset_tool/utils/fileio.py +++ b/superset_tool/utils/fileio.py @@ -93,6 +93,72 @@ def save_and_unpack_dashboard( logger.error(f"Ошибка обработки дашборда: {str(e)}", exc_info=True) raise RuntimeError(f"Failed to unpack dashboard: {str(e)}") from e +def print_directory(root_dir): + if not os.path.isdir(root_dir): + print(f"Error: '{root_dir}' is not a valid directory") + return + + # Печатаем корневую директорию + print(f"{root_dir}/") + + # Получаем список элементов в корневой директории + with os.scandir(root_dir) as entries: + for entry in entries: + # Определяем отступ и форматирование + line = " ├── " if entry.name != sorted(os.listdir(root_dir))[-1] else " └── " + suffix = "/" if entry.is_dir() else "" + print(f"{line}{entry.name}{suffix}") + +def validate_directory_structure(root_dir): + # Проверяем корневую папку + root_items = os.listdir(root_dir) + if len(root_items) != 1: + return False + + subdir_name = root_items[0] + subdir_path = os.path.join(root_dir, subdir_name) + if not os.path.isdir(subdir_path): + return False + + # Проверяем вложенную папку + subdir_items = os.listdir(subdir_path) + + # Проверяем наличие metadata.yaml + if 'metadata.yaml' not in subdir_items: + return False + if not os.path.isfile(os.path.join(subdir_path, 'metadata.yaml')): + return False + + # Проверяем допустимые папки + allowed_folders = {'databases', 'datasets', 'charts', 'dashboards'} + found_folders = set() + + for item in subdir_items: + item_path = os.path.join(subdir_path, item) + + # Пропускаем файл метаданных + if item == 'metadata.yaml': + continue + + # Проверяем что элемент является папкой + if not os.path.isdir(item_path): + return False + + # Проверяем допустимость имени папки + if item not in allowed_folders: + return False + + # Проверяем уникальность папки + if item in found_folders: + return False + found_folders.add(item) + + # Проверяем количество папок + if not 1 <= len(found_folders) <= 4: + return False + + return True + def create_dashboard_export(zip_name, source_paths, exclude_extensions=None, compress_type=zipfile.ZIP_DEFLATED, @@ -107,6 +173,12 @@ def create_dashboard_export(zip_name, source_paths, compress_type: Тип сжатия (по умолчанию ZIP_DEFLATED) """ logger = logger or SupersetLogger(name="fileio", console=False) + + for path in source_paths: + if not validate_directory_structure(path): + logger.error(f"Некорректная структура директории: {path} [1]") + logger.error(print_directory(path)) + logger.info(f"Упаковываем дашборд {source_paths} в {zip_name}") try: exclude_ext = [ext.lower() for ext in exclude_extensions] if exclude_extensions else []