add errors
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,4 +6,6 @@ dashboards/
|
|||||||
*.txt
|
*.txt
|
||||||
*.zip
|
*.zip
|
||||||
keyring passwords.py
|
keyring passwords.py
|
||||||
Logs/
|
Logs/
|
||||||
|
patch1.patch
|
||||||
|
test postman.py
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from .exceptions import *
|
|||||||
from .models import SupersetConfig
|
from .models import SupersetConfig
|
||||||
from .utils.logger import SupersetLogger
|
from .utils.logger import SupersetLogger
|
||||||
|
|
||||||
|
|
||||||
class SupersetClient:
|
class SupersetClient:
|
||||||
def __init__(self, config: SupersetConfig):
|
def __init__(self, config: SupersetConfig):
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -238,22 +237,14 @@ class SupersetClient:
|
|||||||
raise SupersetAPIError(f"Export failed: {str(e)}") from e
|
raise SupersetAPIError(f"Export failed: {str(e)}") from e
|
||||||
|
|
||||||
def import_dashboard(self, zip_path) -> Dict:
|
def import_dashboard(self, zip_path) -> Dict:
|
||||||
"""Импортирует дашборд в Superset из ZIP-архива.
|
"""Импортирует дашборд в Superset из ZIP-архива с детальной обработкой ошибок.
|
||||||
|
|
||||||
Параметры:
|
Параметры:
|
||||||
zip_path (Path): Путь к ZIP-файлу с дашбордом для импорта
|
zip_path (Union[str, Path]): Путь к ZIP-файлу с дашбордом
|
||||||
|
|
||||||
Возвращает:
|
Возвращает:
|
||||||
dict: Ответ API в формате JSON с результатами импорта
|
dict: Ответ API в формате JSON с результатами импорта
|
||||||
|
|
||||||
Исключения:
|
|
||||||
RuntimeError: Вызывается при:
|
|
||||||
- Ошибках сети/соединения
|
|
||||||
- Невалидном формате ZIP-архива
|
|
||||||
- Конфликте прав доступа
|
|
||||||
- Ошибках сервера (status code >= 400)
|
|
||||||
- Попытке перезаписи без соответствующих прав
|
|
||||||
|
|
||||||
Пример использования:
|
Пример использования:
|
||||||
result = client.import_dashboard(Path("my_dashboard.zip"))
|
result = client.import_dashboard(Path("my_dashboard.zip"))
|
||||||
print(f"Импортирован дашборд: {result['title']}")
|
print(f"Импортирован дашборд: {result['title']}")
|
||||||
@@ -269,36 +260,76 @@ class SupersetClient:
|
|||||||
"""
|
"""
|
||||||
url = f"{self.config.base_url}/dashboard/import/"
|
url = f"{self.config.base_url}/dashboard/import/"
|
||||||
self.logger.debug(f"Импортируем дашборд ID {zip_path} на {url}...")
|
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:
|
try:
|
||||||
response.raise_for_status()
|
if not Path(zip_path).exists():
|
||||||
self.logger.info(f"Дашборд импортирован успешно")
|
raise FileNotFoundError(f"Файл не найден: {zip_path}")
|
||||||
return response.json()
|
|
||||||
except requests.exceptions.HTTPError as e:
|
if not zipfile.is_zipfile(zip_path):
|
||||||
self.logger.error(f"Ошибка при импорте: {str(e)}", exc_info=True)
|
raise InvalidZipFormatError(f"Файл не является ZIP-архивом: {zip_path}")
|
||||||
error_detail = f"{e.response.status_code} {e.response.reason}"
|
|
||||||
if e.response.text:
|
# Дополнительная проверка содержимого архива
|
||||||
error_detail += f"\nТело ответа: {e.response.text}"
|
with zipfile.ZipFile(zip_path) as zf:
|
||||||
raise RuntimeError(f"Ошибка импорта: {error_detail}") from e
|
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
|
||||||
|
|||||||
@@ -11,4 +11,22 @@ class ExportError(SupersetToolError):
|
|||||||
"""Dashboard export errors"""
|
"""Dashboard export errors"""
|
||||||
|
|
||||||
class ImportError(SupersetToolError):
|
class ImportError(SupersetToolError):
|
||||||
"""Dashboard import errors"""
|
"""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"
|
||||||
@@ -93,6 +93,72 @@ def save_and_unpack_dashboard(
|
|||||||
logger.error(f"Ошибка обработки дашборда: {str(e)}", exc_info=True)
|
logger.error(f"Ошибка обработки дашборда: {str(e)}", exc_info=True)
|
||||||
raise RuntimeError(f"Failed to unpack dashboard: {str(e)}") from e
|
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,
|
def create_dashboard_export(zip_name, source_paths,
|
||||||
exclude_extensions=None,
|
exclude_extensions=None,
|
||||||
compress_type=zipfile.ZIP_DEFLATED,
|
compress_type=zipfile.ZIP_DEFLATED,
|
||||||
@@ -107,6 +173,12 @@ def create_dashboard_export(zip_name, source_paths,
|
|||||||
compress_type: Тип сжатия (по умолчанию ZIP_DEFLATED)
|
compress_type: Тип сжатия (по умолчанию ZIP_DEFLATED)
|
||||||
"""
|
"""
|
||||||
logger = logger or SupersetLogger(name="fileio", console=False)
|
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}")
|
logger.info(f"Упаковываем дашборд {source_paths} в {zip_name}")
|
||||||
try:
|
try:
|
||||||
exclude_ext = [ext.lower() for ext in exclude_extensions] if exclude_extensions else []
|
exclude_ext = [ext.lower() for ext in exclude_extensions] if exclude_extensions else []
|
||||||
|
|||||||
Reference in New Issue
Block a user