semantic markup update
This commit is contained in:
@@ -36,9 +36,10 @@ class SupersetClient:
|
||||
# @PARAM: config (SupersetConfig) - Конфигурация подключения.
|
||||
# @PARAM: logger (Optional[SupersetLogger]) - Экземпляр логгера.
|
||||
def __init__(self, config: SupersetConfig, logger: Optional[SupersetLogger] = None):
|
||||
self.logger = logger or SupersetLogger(name="SupersetClient")
|
||||
self.logger.info("[SupersetClient.__init__][Enter] Initializing SupersetClient.")
|
||||
self._validate_config(config)
|
||||
with belief_scope("__init__"):
|
||||
self.logger = logger or SupersetLogger(name="SupersetClient")
|
||||
self.logger.info("[SupersetClient.__init__][Enter] Initializing SupersetClient.")
|
||||
self._validate_config(config)
|
||||
self.config = config
|
||||
self.network = APIClient(
|
||||
config=config.dict(),
|
||||
@@ -57,19 +58,21 @@ class SupersetClient:
|
||||
# @THROW: TypeError - Если `config` не является экземпляром `SupersetConfig`.
|
||||
# @PARAM: config (SupersetConfig) - Объект для проверки.
|
||||
def _validate_config(self, config: SupersetConfig) -> None:
|
||||
self.logger.debug("[_validate_config][Enter] Validating SupersetConfig.")
|
||||
assert isinstance(config, SupersetConfig), "Конфигурация должна быть экземпляром SupersetConfig"
|
||||
self.logger.debug("[_validate_config][Exit] Config is valid.")
|
||||
with belief_scope("_validate_config"):
|
||||
self.logger.debug("[_validate_config][Enter] Validating SupersetConfig.")
|
||||
assert isinstance(config, SupersetConfig), "Конфигурация должна быть экземпляром SupersetConfig"
|
||||
self.logger.debug("[_validate_config][Exit] Config is valid.")
|
||||
# [/DEF:_validate_config:Function]
|
||||
|
||||
@property
|
||||
# [DEF:headers:Function]
|
||||
# @PURPOSE: Возвращает базовые HTTP-заголовки, используемые сетевым клиентом.
|
||||
# @PRE: self.network должен быть инициализирован.
|
||||
# @POST: Возвращаемый словарь содержит актуальные заголовки, включая токен авторизации.
|
||||
def headers(self) -> dict:
|
||||
# [DEF:headers:Function]
|
||||
# @PURPOSE: Возвращает базовые HTTP-заголовки, используемые сетевым клиентом.
|
||||
# @PRE: self.network должен быть инициализирован.
|
||||
# @POST: Возвращаемый словарь содержит актуальные заголовки, включая токен авторизации.
|
||||
return self.network.headers
|
||||
# [/DEF:headers:Function]
|
||||
with belief_scope("headers"):
|
||||
return self.network.headers
|
||||
# [/DEF:headers:Function]
|
||||
|
||||
# [DEF:get_dashboards:Function]
|
||||
# @PURPOSE: Получает полный список дашбордов, автоматически обрабатывая пагинацию.
|
||||
@@ -81,10 +84,19 @@ class SupersetClient:
|
||||
# @PARAM: query (Optional[Dict]) - Дополнительные параметры запроса для API.
|
||||
# @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список дашбордов).
|
||||
def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
|
||||
assert self.network, "[get_dashboards][PRE] Network client must be initialized."
|
||||
self.logger.info("[get_dashboards][Enter] Fetching dashboards.")
|
||||
validated_query = self._validate_query_params(query or {})
|
||||
if 'columns' not in validated_query:
|
||||
with belief_scope("get_dashboards"):
|
||||
assert self.network, "[get_dashboards][PRE] Network client must be initialized."
|
||||
self.logger.info("[get_dashboards][Enter] Fetching dashboards.")
|
||||
validated_query = self._validate_query_params(query or {})
|
||||
if 'columns' not in validated_query:
|
||||
validated_query['columns'] = ["slug", "id", "changed_on_utc", "dashboard_title", "published"]
|
||||
total_count = self._fetch_total_object_count(endpoint="/dashboard/")
|
||||
paginated_data = self._fetch_all_pages(
|
||||
endpoint="/dashboard/",
|
||||
pagination_options={"base_query": validated_query, "total_count": total_count, "results_field": "result"},
|
||||
)
|
||||
self.logger.info("[get_dashboards][Exit] Found %d dashboards.", total_count)
|
||||
return total_count, paginated_data
|
||||
validated_query['columns'] = ["slug", "id", "changed_on_utc", "dashboard_title", "published"]
|
||||
total_count = self._fetch_total_object_count(endpoint="/dashboard/")
|
||||
paginated_data = self._fetch_all_pages(
|
||||
@@ -104,20 +116,21 @@ class SupersetClient:
|
||||
# @PARAM: dashboard_id (int) - ID дашборда для экспорта.
|
||||
# @RETURN: Tuple[bytes, str] - Бинарное содержимое ZIP-архива и имя файла.
|
||||
def export_dashboard(self, dashboard_id: int) -> Tuple[bytes, str]:
|
||||
assert isinstance(dashboard_id, int) and dashboard_id > 0, "[export_dashboard][PRE] dashboard_id must be a positive integer."
|
||||
self.logger.info("[export_dashboard][Enter] Exporting dashboard %s.", dashboard_id)
|
||||
response = self.network.request(
|
||||
method="GET",
|
||||
endpoint="/dashboard/export/",
|
||||
params={"q": json.dumps([dashboard_id])},
|
||||
stream=True,
|
||||
raw_response=True,
|
||||
)
|
||||
response = cast(Response, response)
|
||||
self._validate_export_response(response, dashboard_id)
|
||||
filename = self._resolve_export_filename(response, dashboard_id)
|
||||
self.logger.info("[export_dashboard][Exit] Exported dashboard %s to %s.", dashboard_id, filename)
|
||||
return response.content, filename
|
||||
with belief_scope("export_dashboard"):
|
||||
assert isinstance(dashboard_id, int) and dashboard_id > 0, "[export_dashboard][PRE] dashboard_id must be a positive integer."
|
||||
self.logger.info("[export_dashboard][Enter] Exporting dashboard %s.", dashboard_id)
|
||||
response = self.network.request(
|
||||
method="GET",
|
||||
endpoint="/dashboard/export/",
|
||||
params={"q": json.dumps([dashboard_id])},
|
||||
stream=True,
|
||||
raw_response=True,
|
||||
)
|
||||
response = cast(Response, response)
|
||||
self._validate_export_response(response, dashboard_id)
|
||||
filename = self._resolve_export_filename(response, dashboard_id)
|
||||
self.logger.info("[export_dashboard][Exit] Exported dashboard %s to %s.", dashboard_id, filename)
|
||||
return response.content, filename
|
||||
# [/DEF:export_dashboard:Function]
|
||||
|
||||
# [DEF:import_dashboard:Function]
|
||||
@@ -134,24 +147,25 @@ class SupersetClient:
|
||||
# @PARAM: dash_slug (Optional[str]) - Slug дашборда для поиска ID, если ID не предоставлен.
|
||||
# @RETURN: Dict - Ответ API в случае успеха.
|
||||
def import_dashboard(self, file_name: Union[str, Path], dash_id: Optional[int] = None, dash_slug: Optional[str] = None) -> Dict:
|
||||
assert file_name, "[import_dashboard][PRE] file_name must be provided."
|
||||
file_path = str(file_name)
|
||||
self._validate_import_file(file_path)
|
||||
try:
|
||||
return self._do_import(file_path)
|
||||
except Exception as exc:
|
||||
self.logger.error("[import_dashboard][Failure] First import attempt failed: %s", exc, exc_info=True)
|
||||
if not self.delete_before_reimport:
|
||||
raise
|
||||
with belief_scope("import_dashboard"):
|
||||
assert file_name, "[import_dashboard][PRE] file_name must be provided."
|
||||
file_path = str(file_name)
|
||||
self._validate_import_file(file_path)
|
||||
try:
|
||||
return self._do_import(file_path)
|
||||
except Exception as exc:
|
||||
self.logger.error("[import_dashboard][Failure] First import attempt failed: %s", exc, exc_info=True)
|
||||
if not self.delete_before_reimport:
|
||||
raise
|
||||
|
||||
target_id = self._resolve_target_id_for_delete(dash_id, dash_slug)
|
||||
if target_id is None:
|
||||
self.logger.error("[import_dashboard][Failure] No ID available for delete-retry.")
|
||||
raise
|
||||
target_id = self._resolve_target_id_for_delete(dash_id, dash_slug)
|
||||
if target_id is None:
|
||||
self.logger.error("[import_dashboard][Failure] No ID available for delete-retry.")
|
||||
raise
|
||||
|
||||
self.delete_dashboard(target_id)
|
||||
self.logger.info("[import_dashboard][State] Deleted dashboard ID %s, retrying import.", target_id)
|
||||
return self._do_import(file_path)
|
||||
self.delete_dashboard(target_id)
|
||||
self.logger.info("[import_dashboard][State] Deleted dashboard ID %s, retrying import.", target_id)
|
||||
return self._do_import(file_path)
|
||||
# [/DEF:import_dashboard:Function]
|
||||
|
||||
# [DEF:_resolve_target_id_for_delete:Function]
|
||||
@@ -163,10 +177,21 @@ class SupersetClient:
|
||||
# @THROW: APIError - В случае ошибки сетевого запроса при поиске по slug.
|
||||
# @RETURN: Optional[int] - Найденный ID или None.
|
||||
def _resolve_target_id_for_delete(self, dash_id: Optional[int], dash_slug: Optional[str]) -> Optional[int]:
|
||||
assert dash_id is not None or dash_slug is not None, "[_resolve_target_id_for_delete][PRE] At least one of ID or slug must be provided."
|
||||
if dash_id is not None:
|
||||
return dash_id
|
||||
if dash_slug is not None:
|
||||
with belief_scope("_resolve_target_id_for_delete"):
|
||||
assert dash_id is not None or dash_slug is not None, "[_resolve_target_id_for_delete][PRE] At least one of ID or slug must be provided."
|
||||
if dash_id is not None:
|
||||
return dash_id
|
||||
if dash_slug is not None:
|
||||
self.logger.debug("[_resolve_target_id_for_delete][State] Resolving ID by slug '%s'.", dash_slug)
|
||||
try:
|
||||
_, candidates = self.get_dashboards(query={"filters": [{"col": "slug", "op": "eq", "value": dash_slug}]})
|
||||
if candidates:
|
||||
target_id = candidates[0]["id"]
|
||||
self.logger.debug("[_resolve_target_id_for_delete][Success] Resolved slug to ID %s.", target_id)
|
||||
return target_id
|
||||
except Exception as e:
|
||||
self.logger.warning("[_resolve_target_id_for_delete][Warning] Could not resolve slug '%s' to ID: %s", dash_slug, e)
|
||||
return None
|
||||
self.logger.debug("[_resolve_target_id_for_delete][State] Resolving ID by slug '%s'.", dash_slug)
|
||||
try:
|
||||
_, candidates = self.get_dashboards(query={"filters": [{"col": "slug", "op": "eq", "value": dash_slug}]})
|
||||
@@ -187,19 +212,20 @@ class SupersetClient:
|
||||
# @PARAM: file_name (Union[str, Path]) - Путь к файлу.
|
||||
# @RETURN: Dict - Ответ API.
|
||||
def _do_import(self, file_name: Union[str, Path]) -> Dict:
|
||||
self.logger.debug(f"[_do_import][State] Uploading file: {file_name}")
|
||||
file_path = Path(file_name)
|
||||
if file_path.exists():
|
||||
self.logger.debug(f"[_do_import][State] File size: {file_path.stat().st_size} bytes")
|
||||
else:
|
||||
self.logger.error(f"[_do_import][Failure] File does not exist: {file_name}")
|
||||
raise FileNotFoundError(f"File does not exist: {file_name}")
|
||||
return self.network.upload_file(
|
||||
endpoint="/dashboard/import/",
|
||||
file_info={"file_obj": file_path, "file_name": file_path.name, "form_field": "formData"},
|
||||
extra_data={"overwrite": "true"},
|
||||
timeout=self.config.timeout * 2,
|
||||
)
|
||||
with belief_scope("_do_import"):
|
||||
self.logger.debug(f"[_do_import][State] Uploading file: {file_name}")
|
||||
file_path = Path(file_name)
|
||||
if file_path.exists():
|
||||
self.logger.debug(f"[_do_import][State] File size: {file_path.stat().st_size} bytes")
|
||||
else:
|
||||
self.logger.error(f"[_do_import][Failure] File does not exist: {file_name}")
|
||||
raise FileNotFoundError(f"File does not exist: {file_name}")
|
||||
return self.network.upload_file(
|
||||
endpoint="/dashboard/import/",
|
||||
file_info={"file_obj": file_path, "file_name": file_path.name, "form_field": "formData"},
|
||||
extra_data={"overwrite": "true"},
|
||||
timeout=self.config.timeout * 2,
|
||||
)
|
||||
# [/DEF:_do_import:Function]
|
||||
|
||||
# [DEF:delete_dashboard:Function]
|
||||
@@ -210,14 +236,15 @@ class SupersetClient:
|
||||
# @THROW: APIError - В случае ошибки сетевого запроса.
|
||||
# @PARAM: dashboard_id (Union[int, str]) - ID или slug дашборда.
|
||||
def delete_dashboard(self, dashboard_id: Union[int, str]) -> None:
|
||||
assert dashboard_id, "[delete_dashboard][PRE] dashboard_id must be provided."
|
||||
self.logger.info("[delete_dashboard][Enter] Deleting dashboard %s.", dashboard_id)
|
||||
response = self.network.request(method="DELETE", endpoint=f"/dashboard/{dashboard_id}")
|
||||
response = cast(Dict, response)
|
||||
if response.get("result", True) is not False:
|
||||
self.logger.info("[delete_dashboard][Success] Dashboard %s deleted.", dashboard_id)
|
||||
else:
|
||||
self.logger.warning("[delete_dashboard][Warning] Unexpected response while deleting %s: %s", dashboard_id, response)
|
||||
with belief_scope("delete_dashboard"):
|
||||
assert dashboard_id, "[delete_dashboard][PRE] dashboard_id must be provided."
|
||||
self.logger.info("[delete_dashboard][Enter] Deleting dashboard %s.", dashboard_id)
|
||||
response = self.network.request(method="DELETE", endpoint=f"/dashboard/{dashboard_id}")
|
||||
response = cast(Dict, response)
|
||||
if response.get("result", True) is not False:
|
||||
self.logger.info("[delete_dashboard][Success] Dashboard %s deleted.", dashboard_id)
|
||||
else:
|
||||
self.logger.warning("[delete_dashboard][Warning] Unexpected response while deleting %s: %s", dashboard_id, response)
|
||||
# [/DEF:delete_dashboard:Function]
|
||||
|
||||
# [DEF:_extract_dashboard_id_from_zip:Function]
|
||||
@@ -228,19 +255,20 @@ class SupersetClient:
|
||||
# @THROW: ImportError - Если не установлен `yaml`.
|
||||
# @RETURN: Optional[int] - ID дашборда или None.
|
||||
def _extract_dashboard_id_from_zip(self, file_name: Union[str, Path]) -> Optional[int]:
|
||||
assert zipfile.is_zipfile(file_name), "[_extract_dashboard_id_from_zip][PRE] file_name must be a valid zip file."
|
||||
try:
|
||||
import yaml
|
||||
with zipfile.ZipFile(file_name, "r") as zf:
|
||||
for name in zf.namelist():
|
||||
if name.endswith("metadata.yaml"):
|
||||
with zf.open(name) as meta_file:
|
||||
meta = yaml.safe_load(meta_file)
|
||||
dash_id = meta.get("dashboard_uuid") or meta.get("dashboard_id")
|
||||
if dash_id: return int(dash_id)
|
||||
except Exception as exc:
|
||||
self.logger.error("[_extract_dashboard_id_from_zip][Failure] %s", exc, exc_info=True)
|
||||
return None
|
||||
with belief_scope("_extract_dashboard_id_from_zip"):
|
||||
assert zipfile.is_zipfile(file_name), "[_extract_dashboard_id_from_zip][PRE] file_name must be a valid zip file."
|
||||
try:
|
||||
import yaml
|
||||
with zipfile.ZipFile(file_name, "r") as zf:
|
||||
for name in zf.namelist():
|
||||
if name.endswith("metadata.yaml"):
|
||||
with zf.open(name) as meta_file:
|
||||
meta = yaml.safe_load(meta_file)
|
||||
dash_id = meta.get("dashboard_uuid") or meta.get("dashboard_id")
|
||||
if dash_id: return int(dash_id)
|
||||
except Exception as exc:
|
||||
self.logger.error("[_extract_dashboard_id_from_zip][Failure] %s", exc, exc_info=True)
|
||||
return None
|
||||
# [/DEF:_extract_dashboard_id_from_zip:Function]
|
||||
|
||||
# [DEF:_extract_dashboard_slug_from_zip:Function]
|
||||
@@ -251,19 +279,20 @@ class SupersetClient:
|
||||
# @THROW: ImportError - Если не установлен `yaml`.
|
||||
# @RETURN: Optional[str] - Slug дашборда или None.
|
||||
def _extract_dashboard_slug_from_zip(self, file_name: Union[str, Path]) -> Optional[str]:
|
||||
assert zipfile.is_zipfile(file_name), "[_extract_dashboard_slug_from_zip][PRE] file_name must be a valid zip file."
|
||||
try:
|
||||
import yaml
|
||||
with zipfile.ZipFile(file_name, "r") as zf:
|
||||
for name in zf.namelist():
|
||||
if name.endswith("metadata.yaml"):
|
||||
with zf.open(name) as meta_file:
|
||||
meta = yaml.safe_load(meta_file)
|
||||
if slug := meta.get("slug"):
|
||||
return str(slug)
|
||||
except Exception as exc:
|
||||
self.logger.error("[_extract_dashboard_slug_from_zip][Failure] %s", exc, exc_info=True)
|
||||
return None
|
||||
with belief_scope("_extract_dashboard_slug_from_zip"):
|
||||
assert zipfile.is_zipfile(file_name), "[_extract_dashboard_slug_from_zip][PRE] file_name must be a valid zip file."
|
||||
try:
|
||||
import yaml
|
||||
with zipfile.ZipFile(file_name, "r") as zf:
|
||||
for name in zf.namelist():
|
||||
if name.endswith("metadata.yaml"):
|
||||
with zf.open(name) as meta_file:
|
||||
meta = yaml.safe_load(meta_file)
|
||||
if slug := meta.get("slug"):
|
||||
return str(slug)
|
||||
except Exception as exc:
|
||||
self.logger.error("[_extract_dashboard_slug_from_zip][Failure] %s", exc, exc_info=True)
|
||||
return None
|
||||
# [/DEF:_extract_dashboard_slug_from_zip:Function]
|
||||
|
||||
# [DEF:_validate_export_response:Function]
|
||||
@@ -274,12 +303,13 @@ class SupersetClient:
|
||||
# @PARAM: response (Response) - HTTP ответ.
|
||||
# @PARAM: dashboard_id (int) - ID дашборда.
|
||||
def _validate_export_response(self, response: Response, dashboard_id: int) -> None:
|
||||
assert isinstance(response, Response), "[_validate_export_response][PRE] response must be a requests.Response object."
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
if "application/zip" not in content_type:
|
||||
raise ExportError(f"Получен не ZIP-архив (Content-Type: {content_type})")
|
||||
if not response.content:
|
||||
raise ExportError("Получены пустые данные при экспорте")
|
||||
with belief_scope("_validate_export_response"):
|
||||
assert isinstance(response, Response), "[_validate_export_response][PRE] response must be a requests.Response object."
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
if "application/zip" not in content_type:
|
||||
raise ExportError(f"Получен не ZIP-архив (Content-Type: {content_type})")
|
||||
if not response.content:
|
||||
raise ExportError("Получены пустые данные при экспорте")
|
||||
# [/DEF:_validate_export_response:Function]
|
||||
|
||||
# [DEF:_resolve_export_filename:Function]
|
||||
@@ -290,14 +320,15 @@ class SupersetClient:
|
||||
# @PARAM: dashboard_id (int) - ID дашборда.
|
||||
# @RETURN: str - Имя файла.
|
||||
def _resolve_export_filename(self, response: Response, dashboard_id: int) -> str:
|
||||
assert isinstance(response, Response), "[_resolve_export_filename][PRE] response must be a requests.Response object."
|
||||
filename = get_filename_from_headers(dict(response.headers))
|
||||
if not filename:
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
|
||||
filename = f"dashboard_export_{dashboard_id}_{timestamp}.zip"
|
||||
self.logger.warning("[_resolve_export_filename][Warning] Generated filename: %s", filename)
|
||||
return filename
|
||||
with belief_scope("_resolve_export_filename"):
|
||||
assert isinstance(response, Response), "[_resolve_export_filename][PRE] response must be a requests.Response object."
|
||||
filename = get_filename_from_headers(dict(response.headers))
|
||||
if not filename:
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
|
||||
filename = f"dashboard_export_{dashboard_id}_{timestamp}.zip"
|
||||
self.logger.warning("[_resolve_export_filename][Warning] Generated filename: %s", filename)
|
||||
return filename
|
||||
# [/DEF:_resolve_export_filename:Function]
|
||||
|
||||
# [DEF:_validate_query_params:Function]
|
||||
@@ -307,9 +338,10 @@ class SupersetClient:
|
||||
# @POST: Возвращает словарь, содержащий базовые параметры пагинации, объединенные с `query`.
|
||||
# @RETURN: Dict - Валидированные параметры.
|
||||
def _validate_query_params(self, query: Optional[Dict]) -> Dict:
|
||||
assert query is None or isinstance(query, dict), "[_validate_query_params][PRE] query must be a dictionary or None."
|
||||
base_query = {"page": 0, "page_size": 1000}
|
||||
return {**base_query, **(query or {})}
|
||||
with belief_scope("_validate_query_params"):
|
||||
assert query is None or isinstance(query, dict), "[_validate_query_params][PRE] query must be a dictionary or None."
|
||||
base_query = {"page": 0, "page_size": 1000}
|
||||
return {**base_query, **(query or {})}
|
||||
# [/DEF:_validate_query_params:Function]
|
||||
|
||||
# [DEF:_fetch_total_object_count:Function]
|
||||
@@ -320,12 +352,13 @@ class SupersetClient:
|
||||
# @THROW: APIError - В случае ошибки сетевого запроса.
|
||||
# @RETURN: int - Количество объектов.
|
||||
def _fetch_total_object_count(self, endpoint: str) -> int:
|
||||
assert endpoint and isinstance(endpoint, str), "[_fetch_total_object_count][PRE] endpoint must be a non-empty string."
|
||||
return self.network.fetch_paginated_count(
|
||||
endpoint=endpoint,
|
||||
query_params={"page": 0, "page_size": 1},
|
||||
count_field="count",
|
||||
)
|
||||
with belief_scope("_fetch_total_object_count"):
|
||||
assert endpoint and isinstance(endpoint, str), "[_fetch_total_object_count][PRE] endpoint must be a non-empty string."
|
||||
return self.network.fetch_paginated_count(
|
||||
endpoint=endpoint,
|
||||
query_params={"page": 0, "page_size": 1},
|
||||
count_field="count",
|
||||
)
|
||||
# [/DEF:_fetch_total_object_count:Function]
|
||||
|
||||
# [DEF:_fetch_all_pages:Function]
|
||||
@@ -337,9 +370,10 @@ class SupersetClient:
|
||||
# @THROW: APIError - В случае ошибки сетевого запроса.
|
||||
# @RETURN: List[Dict] - Список всех объектов.
|
||||
def _fetch_all_pages(self, endpoint: str, pagination_options: Dict) -> List[Dict]:
|
||||
assert endpoint and isinstance(endpoint, str), "[_fetch_all_pages][PRE] endpoint must be a non-empty string."
|
||||
assert isinstance(pagination_options, dict), "[_fetch_all_pages][PRE] pagination_options must be a dictionary."
|
||||
return self.network.fetch_paginated_data(endpoint=endpoint, pagination_options=pagination_options)
|
||||
with belief_scope("_fetch_all_pages"):
|
||||
assert endpoint and isinstance(endpoint, str), "[_fetch_all_pages][PRE] endpoint must be a non-empty string."
|
||||
assert isinstance(pagination_options, dict), "[_fetch_all_pages][PRE] pagination_options must be a dictionary."
|
||||
return self.network.fetch_paginated_data(endpoint=endpoint, pagination_options=pagination_options)
|
||||
# [/DEF:_fetch_all_pages:Function]
|
||||
|
||||
# [DEF:_validate_import_file:Function]
|
||||
@@ -350,12 +384,13 @@ class SupersetClient:
|
||||
# @THROW: InvalidZipFormatError - Если файл не является ZIP или не содержит `metadata.yaml`.
|
||||
# @PARAM: zip_path (Union[str, Path]) - Путь к файлу.
|
||||
def _validate_import_file(self, zip_path: Union[str, Path]) -> None:
|
||||
assert zip_path, "[_validate_import_file][PRE] zip_path must be provided."
|
||||
path = Path(zip_path)
|
||||
assert path.exists(), f"Файл {zip_path} не существует"
|
||||
assert zipfile.is_zipfile(path), f"Файл {zip_path} не является ZIP-архивом"
|
||||
with zipfile.ZipFile(path, "r") as zf:
|
||||
assert any(n.endswith("metadata.yaml") for n in zf.namelist()), f"Архив {zip_path} не содержит 'metadata.yaml'"
|
||||
with belief_scope("_validate_import_file"):
|
||||
assert zip_path, "[_validate_import_file][PRE] zip_path must be provided."
|
||||
path = Path(zip_path)
|
||||
assert path.exists(), f"Файл {zip_path} не существует"
|
||||
assert zipfile.is_zipfile(path), f"Файл {zip_path} не является ZIP-архивом"
|
||||
with zipfile.ZipFile(path, "r") as zf:
|
||||
assert any(n.endswith("metadata.yaml") for n in zf.namelist()), f"Архив {zip_path} не содержит 'metadata.yaml'"
|
||||
# [/DEF:_validate_import_file:Function]
|
||||
|
||||
# [DEF:get_datasets:Function]
|
||||
@@ -368,17 +403,18 @@ class SupersetClient:
|
||||
# @THROW: APIError - В случае ошибки сетевого запроса.
|
||||
# @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список датасетов).
|
||||
def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
|
||||
assert self.network, "[get_datasets][PRE] Network client must be initialized."
|
||||
self.logger.info("[get_datasets][Enter] Fetching datasets.")
|
||||
validated_query = self._validate_query_params(query)
|
||||
with belief_scope("get_datasets"):
|
||||
assert self.network, "[get_datasets][PRE] Network client must be initialized."
|
||||
self.logger.info("[get_datasets][Enter] Fetching datasets.")
|
||||
validated_query = self._validate_query_params(query)
|
||||
|
||||
total_count = self._fetch_total_object_count(endpoint="/dataset/")
|
||||
paginated_data = self._fetch_all_pages(
|
||||
endpoint="/dataset/",
|
||||
pagination_options={"base_query": validated_query, "total_count": total_count, "results_field": "result"},
|
||||
)
|
||||
self.logger.info("[get_datasets][Exit] Found %d datasets.", total_count)
|
||||
return total_count, paginated_data
|
||||
total_count = self._fetch_total_object_count(endpoint="/dataset/")
|
||||
paginated_data = self._fetch_all_pages(
|
||||
endpoint="/dataset/",
|
||||
pagination_options={"base_query": validated_query, "total_count": total_count, "results_field": "result"},
|
||||
)
|
||||
self.logger.info("[get_datasets][Exit] Found %d datasets.", total_count)
|
||||
return total_count, paginated_data
|
||||
# [/DEF:get_datasets:Function]
|
||||
|
||||
# [DEF:get_databases:Function]
|
||||
@@ -391,18 +427,19 @@ class SupersetClient:
|
||||
# @THROW: APIError - В случае ошибки сетевого запроса.
|
||||
# @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список баз данных).
|
||||
def get_databases(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
|
||||
assert self.network, "[get_databases][PRE] Network client must be initialized."
|
||||
self.logger.info("[get_databases][Enter] Fetching databases.")
|
||||
validated_query = self._validate_query_params(query or {})
|
||||
if 'columns' not in validated_query:
|
||||
validated_query['columns'] = []
|
||||
total_count = self._fetch_total_object_count(endpoint="/database/")
|
||||
paginated_data = self._fetch_all_pages(
|
||||
endpoint="/database/",
|
||||
pagination_options={"base_query": validated_query, "total_count": total_count, "results_field": "result"},
|
||||
)
|
||||
self.logger.info("[get_databases][Exit] Found %d databases.", total_count)
|
||||
return total_count, paginated_data
|
||||
with belief_scope("get_databases"):
|
||||
assert self.network, "[get_databases][PRE] Network client must be initialized."
|
||||
self.logger.info("[get_databases][Enter] Fetching databases.")
|
||||
validated_query = self._validate_query_params(query or {})
|
||||
if 'columns' not in validated_query:
|
||||
validated_query['columns'] = []
|
||||
total_count = self._fetch_total_object_count(endpoint="/database/")
|
||||
paginated_data = self._fetch_all_pages(
|
||||
endpoint="/database/",
|
||||
pagination_options={"base_query": validated_query, "total_count": total_count, "results_field": "result"},
|
||||
)
|
||||
self.logger.info("[get_databases][Exit] Found %d databases.", total_count)
|
||||
return total_count, paginated_data
|
||||
# [/DEF:get_databases:Function]
|
||||
|
||||
# [DEF:get_dataset:Function]
|
||||
@@ -414,12 +451,13 @@ class SupersetClient:
|
||||
# @THROW: APIError - В случае ошибки сетевого запроса или если датасет не найден.
|
||||
# @RETURN: Dict - Информация о датасете.
|
||||
def get_dataset(self, dataset_id: int) -> Dict:
|
||||
assert isinstance(dataset_id, int) and dataset_id > 0, "[get_dataset][PRE] dataset_id must be a positive integer."
|
||||
self.logger.info("[get_dataset][Enter] Fetching dataset %s.", dataset_id)
|
||||
response = self.network.request(method="GET", endpoint=f"/dataset/{dataset_id}")
|
||||
response = cast(Dict, response)
|
||||
self.logger.info("[get_dataset][Exit] Got dataset %s.", dataset_id)
|
||||
return response
|
||||
with belief_scope("get_dataset"):
|
||||
assert isinstance(dataset_id, int) and dataset_id > 0, "[get_dataset][PRE] dataset_id must be a positive integer."
|
||||
self.logger.info("[get_dataset][Enter] Fetching dataset %s.", dataset_id)
|
||||
response = self.network.request(method="GET", endpoint=f"/dataset/{dataset_id}")
|
||||
response = cast(Dict, response)
|
||||
self.logger.info("[get_dataset][Exit] Got dataset %s.", dataset_id)
|
||||
return response
|
||||
# [/DEF:get_dataset:Function]
|
||||
|
||||
# [DEF:get_database:Function]
|
||||
@@ -431,12 +469,13 @@ class SupersetClient:
|
||||
# @THROW: APIError - В случае ошибки сетевого запроса или если база данных не найдена.
|
||||
# @RETURN: Dict - Информация о базе данных.
|
||||
def get_database(self, database_id: int) -> Dict:
|
||||
assert isinstance(database_id, int) and database_id > 0, "[get_database][PRE] database_id must be a positive integer."
|
||||
self.logger.info("[get_database][Enter] Fetching database %s.", database_id)
|
||||
response = self.network.request(method="GET", endpoint=f"/database/{database_id}")
|
||||
response = cast(Dict, response)
|
||||
self.logger.info("[get_database][Exit] Got database %s.", database_id)
|
||||
return response
|
||||
with belief_scope("get_database"):
|
||||
assert isinstance(database_id, int) and database_id > 0, "[get_database][PRE] database_id must be a positive integer."
|
||||
self.logger.info("[get_database][Enter] Fetching database %s.", database_id)
|
||||
response = self.network.request(method="GET", endpoint=f"/database/{database_id}")
|
||||
response = cast(Dict, response)
|
||||
self.logger.info("[get_database][Exit] Got database %s.", database_id)
|
||||
return response
|
||||
# [/DEF:get_database:Function]
|
||||
|
||||
# [DEF:update_dataset:Function]
|
||||
@@ -449,18 +488,19 @@ class SupersetClient:
|
||||
# @THROW: APIError - В случае ошибки сетевого запроса.
|
||||
# @RETURN: Dict - Ответ API.
|
||||
def update_dataset(self, dataset_id: int, data: Dict) -> Dict:
|
||||
assert isinstance(dataset_id, int) and dataset_id > 0, "[update_dataset][PRE] dataset_id must be a positive integer."
|
||||
assert isinstance(data, dict) and data, "[update_dataset][PRE] data must be a non-empty dictionary."
|
||||
self.logger.info("[update_dataset][Enter] Updating dataset %s.", dataset_id)
|
||||
response = self.network.request(
|
||||
method="PUT",
|
||||
endpoint=f"/dataset/{dataset_id}",
|
||||
data=json.dumps(data),
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
response = cast(Dict, response)
|
||||
self.logger.info("[update_dataset][Exit] Updated dataset %s.", dataset_id)
|
||||
return response
|
||||
with belief_scope("update_dataset"):
|
||||
assert isinstance(dataset_id, int) and dataset_id > 0, "[update_dataset][PRE] dataset_id must be a positive integer."
|
||||
assert isinstance(data, dict) and data, "[update_dataset][PRE] data must be a non-empty dictionary."
|
||||
self.logger.info("[update_dataset][Enter] Updating dataset %s.", dataset_id)
|
||||
response = self.network.request(
|
||||
method="PUT",
|
||||
endpoint=f"/dataset/{dataset_id}",
|
||||
data=json.dumps(data),
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
response = cast(Dict, response)
|
||||
self.logger.info("[update_dataset][Exit] Updated dataset %s.", dataset_id)
|
||||
return response
|
||||
# [/DEF:update_dataset:Function]
|
||||
|
||||
# [/DEF:SupersetClient:Class]
|
||||
|
||||
Reference in New Issue
Block a user