This commit is contained in:
Volobuev Andrey
2025-04-08 16:38:58 +03:00
parent 6ffc432b42
commit 625b50a6d2
4 changed files with 115 additions and 102 deletions

View File

@@ -2,7 +2,7 @@ from superset_tool.models import SupersetConfig
from superset_tool.client import SupersetClient from superset_tool.client import SupersetClient
from superset_tool.utils.logger import SupersetLogger from superset_tool.utils.logger import SupersetLogger
from superset_tool.exceptions import AuthenticationError from superset_tool.exceptions import AuthenticationError
from superset_tool.utils.fileio import save_and_unpack_dashboard, update_db_yaml, create_dashboard_export from superset_tool.utils.fileio import save_and_unpack_dashboard, update_db_yaml, create_dashboard_export, create_temp_file
import os import os
import keyring import keyring
from pathlib import Path from pathlib import Path
@@ -21,9 +21,9 @@ database_config_click={"new":
"sqlalchemy_uri": "clickhousedb+connect://clicketl:XXXXXXXXXX@rgm-s-khclk.hq.root.ad:443/dm", "sqlalchemy_uri": "clickhousedb+connect://clicketl:XXXXXXXXXX@rgm-s-khclk.hq.root.ad:443/dm",
"uuid": "b9b67cb5-9874-4dc6-87bd-354fc33be6f9", "uuid": "b9b67cb5-9874-4dc6-87bd-354fc33be6f9",
"database_uuid": "b9b67cb5-9874-4dc6-87bd-354fc33be6f9", "database_uuid": "b9b67cb5-9874-4dc6-87bd-354fc33be6f9",
"allow_ctas": "true", "allow_ctas": "false",
"allow_cvas": "true", "allow_cvas": "false",
"allow_dml": "true" "allow_dml": "false"
}, },
"old": { "old": {
"database_name": "Dev Clickhouse", "database_name": "Dev Clickhouse",
@@ -105,32 +105,36 @@ prod_client = SupersetClient(prod_config)
from_c = dev_client from_c = dev_client
to_c = sandbox_client to_c = sandbox_client
dashboard_slug = "FI0050" dashboard_slug = "FI0050"
#dashboard_id = 53 dashboard_id = 53
dashboard_meta = from_c.get_dashboard(dashboard_slug) dashboard_meta = from_c.get_dashboard(dashboard_slug)
#print(dashboard_meta) #print(dashboard_meta)
print(dashboard_meta["dashboard_title"]) #print(dashboard_meta["dashboard_title"])
dashboard_id = dashboard_meta["id"] dashboard_id = dashboard_meta["id"]
zip_content, filename = from_c.export_dashboard(dashboard_id, logger=logger)
superset_repo = Path("H:\\dev\\dashboards\\")
# print(f"Экспортируем дашборд ID = {dashboard_id}...")
# #Сохранение и распаковка
zip_path, unpacked_path = save_and_unpack_dashboard(
zip_content=zip_content,
original_filename=filename,
unpack=True,
logger=logger,
output_dir=os.path.join(superset_repo,dashboard_slug)
)
dest_path = os.path.join(superset_repo,dashboard_slug)
source_path = os.path.join(unpacked_path,Path(filename).stem)
update_db_yaml(database_config_click, path = source_path, logger=logger) with create_temp_file(suffix='.dir', logger=logger) as temp_root:
update_db_yaml(database_config_gp, path = source_path, logger=logger) # Экспорт дашборда во временную директорию
zip_content, filename = from_c.export_dashboard(dashboard_id, logger=logger)
create_dashboard_export(f"{dashboard_slug}.zip",[source_path],logger=logger) # Сохранение и распаковка во временную директорию
zip_path, unpacked_path = save_and_unpack_dashboard(
zip_content=zip_content,
original_filename=filename,
unpack=True,
logger=logger,
output_dir=temp_root
)
zip_path = Path(f"{dashboard_slug}.zip") # Обновление конфигураций
to_c.import_dashboard(zip_path) source_path = unpacked_path / Path(filename).stem
update_db_yaml(database_config_click, path=source_path, logger=logger)
update_db_yaml(database_config_gp, path=source_path, logger=logger)
# Создание нового экспорта во временной директории
temp_zip = temp_root / f"{dashboard_slug}.zip"
create_dashboard_export(temp_zip, [source_path], logger=logger)
# Импорт обновленного дашборда
to_c.import_dashboard(temp_zip)

View File

@@ -48,7 +48,8 @@ class SupersetClient:
) )
response.raise_for_status() response.raise_for_status()
self.access_token = response.json()["access_token"] self.access_token = response.json()["access_token"]
self.logger.info(f"Токен Bearer {self.access_token} получен c {login_url}") self.logger.info(
f"Токен Bearer {self.access_token} получен c {login_url}")
# Затем получаем CSRF токен с использованием access_token # Затем получаем CSRF токен с использованием access_token
csrf_url = f"{self.config.base_url}/security/csrf_token/" csrf_url = f"{self.config.base_url}/security/csrf_token/"
@@ -61,15 +62,16 @@ class SupersetClient:
response.raise_for_status() response.raise_for_status()
self.csrf_token = response.json()["result"] self.csrf_token = response.json()["result"]
self.logger.info(f"Токен CSRF {self.csrf_token} получен c {csrf_url}") self.logger.info(
f"Токен CSRF {self.csrf_token} получен c {csrf_url}")
except HTTPError as e: except HTTPError as e:
if e.response.status_code == 401: if e.response.status_code == 401:
error_msg = f"Неверные данные для аутенфикации для {login_url}" if "login" in e.request.url else f"Не удалось получить CSRF токен с {csrf_url}" error_msg = f"Неверные данные для аутенфикации для {login_url}" if "login" in e.request.url else f"Не удалось получить CSRF токен с {csrf_url}"
self.logger.error(f"Ошибка получения: {error_msg}") self.logger.error(f"Ошибка получения: {error_msg}")
raise AuthenticationError(f"{error_msg}. Проверь данные аутенфикации") from e raise AuthenticationError(
f"{error_msg}. Проверь данные аутенфикации") from e
raise raise
@property @property
def headers(self): def headers(self):
return { return {
@@ -79,14 +81,15 @@ class SupersetClient:
"Content-Type": "application/json" "Content-Type": "application/json"
} }
def get_dashboard(self, dashboard_id_or_slug: str ) -> Dict: def get_dashboard(self, dashboard_id_or_slug: str) -> Dict:
""" """
Получаем информацию по дашборду (если передан dashboard_id_or_slug), либо по всем дашбордам, если параметр не передан Получаем информацию по дашборду (если передан dashboard_id_or_slug)
Параметры: Параметры:
:dashboard_id_or_slug - id или короткая ссылка :dashboard_id_or_slug - id или короткая ссылка
""" """
url = f"{self.config.base_url}/dashboard/{dashboard_id_or_slug}" url = f"{self.config.base_url}/dashboard/{dashboard_id_or_slug}"
self.logger.debug(f"Получаем информацию по дашборду с /{url}...") self.logger.debug(f"Получаем информацию по дашборду с /{url}...")
try: try:
response = self.session.get( response = self.session.get(
url, url,
@@ -94,12 +97,14 @@ class SupersetClient:
timeout=self.config.timeout timeout=self.config.timeout
) )
response.raise_for_status() response.raise_for_status()
self.logger.info(f"ОК - Получили информацию по дашборду с {url}") self.logger.info(f"ОК - Получили информацию по дашборду с {response.url}")
return response.json()["result"] return response.json()["result"]
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка при получении информации о дашборде: {str(e)}", exc_info=True) self.logger.error(
raise SupersetAPIError(f"Ошибка при получении информации о дашборде: {str(e)}") from e f"Ошибка при получении информации о дашборде: {str(e)}", exc_info=True)
raise SupersetAPIError(
f"Ошибка при получении информации о дашборде: {str(e)}") from e
def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
""" """
@@ -117,19 +122,24 @@ class SupersetClient:
all_results: List[Dict] = [] all_results: List[Dict] = []
total_count: int = 0 total_count: int = 0
current_page: int = 0 current_page: int = 0
q_param = '{ "columns": [ "id" ], "page": 0, "page_size": 20}'
try: try:
total_count = self.session.get( response = self.session.get(
url, url=f"{url}?q={q_param}",
#q=modified_query, #params={"q": json.dumps(default_query)}, # Передаем такой body, иначе на prodta отдает 1 дашборд
headers=self.headers, headers=self.headers,
timeout=self.config.timeout timeout=self.config.timeout
).json()['count'] )
self.logger.info(f"ОК - Получили кол-во дашбордов ({total_count}) с {url}") total_count = response.json()['count']
self.logger.info(
f"ОК - Получили кол-во дашбордов ({total_count}) с {url}")
self.logger.info(f"Запрос - {response.url}")
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка при получении кол-ва дашбордов: {str(e)}", exc_info=True) self.logger.error(
raise SupersetAPIError(f"Ошибка при получении кол-ва дашбордов: {str(e)}") from e f"Ошибка при получении кол-ва дашбордов: {str(e)}", exc_info=True)
#Инициализация параметров запроса с учетом переданного query raise SupersetAPIError(
f"Ошибка при получении кол-ва дашбордов: {str(e)}") from e
# Инициализация параметров запроса с учетом переданного query
if query: if query:
modified_query = query.copy() modified_query = query.copy()
@@ -150,18 +160,15 @@ class SupersetClient:
page_size = modified_query["page_size"] page_size = modified_query["page_size"]
total_pages = (total_count + page_size - 1) // page_size total_pages = (total_count + page_size - 1) // page_size
try: try:
while current_page < total_pages: while current_page < total_pages:
modified_query["page"] = current_page modified_query["page"] = current_page
response = self.session.get( response = self.session.get(
url, url,
headers=self.headers, headers=self.headers,
params={"q": json.dumps(modified_query)} , params={"q": json.dumps(modified_query)},
timeout=self.config.timeout timeout=self.config.timeout
) )
response.raise_for_status() response.raise_for_status()
@@ -170,11 +177,13 @@ class SupersetClient:
current_page += 1 current_page += 1
self.logger.info(f"ОК - Получили информацию по дашбордам с {url}") self.logger.info(f"ОК - Получили информацию по дашбордам с {url}")
# Проверка, достигли ли последней страницы # Проверка, достигли ли последней страницы
return total_count, all_results return total_count, all_results
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка при получении информации о дашбордах: {str(e)}", exc_info=True) self.logger.error(
raise SupersetAPIError(f"Ошибка при получении информации о дашбордах: {str(e)}") from e f"Ошибка при получении информации о дашбордах: {str(e)}", exc_info=True)
raise SupersetAPIError(
f"Ошибка при получении информации о дашбордах: {str(e)}") from e
def export_dashboard(self, dashboard_id: int, logger: Optional[SupersetLogger] = None) -> Tuple[bytes, str]: def export_dashboard(self, dashboard_id: int, logger: Optional[SupersetLogger] = None) -> Tuple[bytes, str]:
"""Экспортирует дашборд из Superset в виде ZIP-архива и возвращает его содержимое с именем файла. """Экспортирует дашборд из Superset в виде ZIP-архива и возвращает его содержимое с именем файла.
@@ -219,7 +228,8 @@ class SupersetClient:
) )
response.raise_for_status() response.raise_for_status()
filename = get_filename_from_headers(response.headers) or f"dashboard_{dashboard_id}.zip" filename = get_filename_from_headers(
response.headers) or f"dashboard_{dashboard_id}.zip"
self.logger.info(f"Дашборд сохранен в {filename}") self.logger.info(f"Дашборд сохранен в {filename}")
return response.content, filename return response.content, filename
@@ -227,7 +237,6 @@ class SupersetClient:
self.logger.error(f"Ошибка при экспорте: {str(e)}", exc_info=True) self.logger.error(f"Ошибка при экспорте: {str(e)}", exc_info=True)
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-архива.
@@ -260,7 +269,8 @@ 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'} headers_without_content_type = {
k: v for k, v in self.headers.items() if k.lower() != 'content-type'}
zip_name = zip_path.name zip_name = zip_path.name
# Подготавливаем данные для multipart/form-data # Подготавливаем данные для multipart/form-data
@@ -275,12 +285,12 @@ class SupersetClient:
# Отправляем запрос # Отправляем запрос
response = self.session.post( response = self.session.post(
url, url,
files=files, files=files,
data={'overwrite': 'true'}, data={'overwrite': 'true'},
headers=headers_without_content_type, headers=headers_without_content_type,
timeout=self.config.timeout * 2 # Longer timeout for imports timeout=self.config.timeout * 2 # Longer timeout for imports
) )
# Обрабатываем ответ # Обрабатываем ответ
try: try:
response.raise_for_status() response.raise_for_status()

View File

@@ -13,24 +13,35 @@ from ..utils.logger import SupersetLogger
@contextmanager @contextmanager
def create_temp_file( def create_temp_file(
content: bytes, content: Optional[bytes] = None,
suffix: str = ".zip", suffix: str = ".zip",
mode: str = 'wb',
logger: Optional[SupersetLogger] = None logger: Optional[SupersetLogger] = None
): ):
"""Контекстный менеджер для создания временных файлов с логированием""" """Расширенный контекстный менеджер для временных файлов/директорий"""
logger = logger or SupersetLogger(name="fileio", console=False) logger = logger or SupersetLogger(name="fileio", console=False)
try: try:
logger.debug(f"Создание временного файла с суффиксом {suffix}") logger.debug(f"Создание временного ресурса с суффиксом {suffix}")
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
tmp.write(content) # Для директорий
tmp.flush() if suffix.startswith('.dir'):
yield Path(tmp.name) with tempfile.TemporaryDirectory(suffix=suffix) as tmp_dir:
logger.debug(f"Создана временная директория: {tmp_dir}")
yield Path(tmp_dir)
# Для файлов
else:
with tempfile.NamedTemporaryFile(suffix=suffix, mode=mode, delete=False) as tmp:
if content:
tmp.write(content)
tmp.flush()
logger.debug(f"Создан временный файл: {tmp.name}")
yield Path(tmp.name)
except Exception as e: except Exception as e:
logger.error( logger.error(f"Ошибка создания временного ресурса: {str(e)}", exc_info=True)
f"Ошибка создания временного файла: {str(e)}", exc_info=True)
raise raise
finally: finally:
if Path(tmp.name).exists(): if 'tmp' in locals() and Path(tmp.name).exists() and not suffix.startswith('.dir'):
Path(tmp.name).unlink() Path(tmp.name).unlink()
logger.debug(f"Временный файл {tmp.name} удален") logger.debug(f"Временный файл {tmp.name} удален")

View File

@@ -65,10 +65,10 @@ def setup_clients():
clients['dev'] = SupersetClient(dev_config) clients['dev'] = SupersetClient(dev_config)
except Exception as e: except Exception as e:
logger.error(f"Ошибка инициализации клиента: {str(e)}") logger.error(f"Ошибка инициализации клиента: {str(e)}")
try: # try:
clients['sbx'] = SupersetClient(sandbox_config) # clients['sbx'] = SupersetClient(sandbox_config)
except Exception as e: # except Exception as e:
logger.error(f"Ошибка инициализации клиента: {str(e)}") # logger.error(f"Ошибка инициализации клиента: {str(e)}")
try: try:
clients['prod'] = SupersetClient(prod_config) clients['prod'] = SupersetClient(prod_config)
except Exception as e: except Exception as e:
@@ -84,19 +84,7 @@ def backup_dashboards(client, env_name, backup_root):
#logger.info(f"Начало бэкапа для окружения {env_name}") #logger.info(f"Начало бэкапа для окружения {env_name}")
#print(client.get_dashboards()) #print(client.get_dashboards())
# dashboard_count,dashboard_meta = client.get_dashboards() print(client.get_dashboard("IM0010"))
# total = 0
# success = 0
# i=1
# for db in dashboard_meta:
# #total += 1
# #print(total)
# if db['slug']:
# success+=1
# print(f"{db['dashboard_title']} {i}. {db['id']}")
# i+=1
# for db in dashboard_meta:
# print(f"DB Id = {db["id"]} DB title = {db["dashboard_title"]} DB SLUG - {db["slug"]}")
@@ -112,11 +100,11 @@ dev_success = backup_dashboards(
# Бэкап для Sandbox # Бэкап для Sandbox
sbx_success = backup_dashboards( # sbx_success = backup_dashboards(
clients['sbx'], # clients['sbx'],
"SBX", # "SBX",
superset_backup_repo # superset_backup_repo
) # )
prod_success = backup_dashboards( prod_success = backup_dashboards(
clients['prod'], clients['prod'],