chore(semantic): remediate backend core contracts

This commit is contained in:
2026-03-15 21:23:44 +03:00
parent 84a2cd5429
commit 0bf55885a8
7 changed files with 166 additions and 49 deletions

View File

@@ -24,13 +24,17 @@ from .config_models import Environment
# [/SECTION]
# [DEF:SupersetClient:Class]
# @TIER: STANDARD
# @PURPOSE: Класс-обёртка над Superset REST API, предоставляющий методы для работы с дашбордами и датасетами.
# @RELATION: [DEPENDS_ON] ->[backend.src.core.utils.network.APIClient]
# @RELATION: [DEPENDS_ON] ->[backend.src.core.config_models.Environment]
class SupersetClient:
# [DEF:__init__:Function]
# @TIER: STANDARD
# @PURPOSE: Инициализирует клиент, проверяет конфигурацию и создает сетевой клиент.
# @PRE: `env` должен быть валидным объектом Environment.
# @POST: Атрибуты `env` и `network` созданы и готовы к работе.
# @PARAM: env (Environment) - Конфигурация окружения.
# @DATA_CONTRACT: Input[Environment] -> self.network[APIClient]
def __init__(self, env: Environment):
with belief_scope("__init__"):
app_logger.info("[SupersetClient.__init__][Enter] Initializing SupersetClient for env %s.", env.name)
@@ -55,10 +59,12 @@ class SupersetClient:
# [/DEF:__init__:Function]
# [DEF:authenticate:Function]
# @TIER: STANDARD
# @PURPOSE: Authenticates the client using the configured credentials.
# @PRE: self.network must be initialized with valid auth configuration.
# @POST: Client is authenticated and tokens are stored.
# @RETURN: Dict[str, str] - Authentication tokens.
# @DATA_CONTRACT: None -> Output[Dict[str, str]]
# @RELATION: [CALLS] ->[self.network.authenticate]
def authenticate(self) -> Dict[str, str]:
with belief_scope("SupersetClient.authenticate"):
return self.network.authenticate()
@@ -66,6 +72,7 @@ class SupersetClient:
@property
# [DEF:headers:Function]
# @TIER: TRIVIAL
# @PURPOSE: Возвращает базовые HTTP-заголовки, используемые сетевым клиентом.
# @PRE: APIClient is initialized and authenticated.
# @POST: Returns a dictionary of HTTP headers.
@@ -77,11 +84,12 @@ class SupersetClient:
# [SECTION: DASHBOARD OPERATIONS]
# [DEF:get_dashboards:Function]
# @TIER: STANDARD
# @PURPOSE: Получает полный список дашбордов, автоматически обрабатывая пагинацию.
# @PARAM: query (Optional[Dict]) - Дополнительные параметры запроса для API.
# @PRE: Client is authenticated.
# @POST: Returns a tuple with total count and list of dashboards.
# @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список дашбордов).
# @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
# @RELATION: [CALLS] ->[self._fetch_all_pages]
def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
with belief_scope("get_dashboards"):
app_logger.info("[get_dashboards][Enter] Fetching dashboards.")
@@ -110,11 +118,12 @@ class SupersetClient:
# [/DEF:get_dashboards:Function]
# [DEF:get_dashboards_page:Function]
# @TIER: STANDARD
# @PURPOSE: Fetches a single dashboards page from Superset without iterating all pages.
# @PARAM: query (Optional[Dict]) - Query with page/page_size and optional columns.
# @PRE: Client is authenticated.
# @POST: Returns total count and one page of dashboards.
# @RETURN: Tuple[int, List[Dict]]
# @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
# @RELATION: [CALLS] ->[self.network.request]
def get_dashboards_page(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
with belief_scope("get_dashboards_page"):
validated_query = self._validate_query_params(query or {})
@@ -146,10 +155,12 @@ class SupersetClient:
# [/DEF:get_dashboards_page:Function]
# [DEF:get_dashboards_summary:Function]
# @TIER: STANDARD
# @PURPOSE: Fetches dashboard metadata optimized for the grid.
# @PRE: Client is authenticated.
# @POST: Returns a list of dashboard metadata summaries.
# @RETURN: List[Dict]
# @DATA_CONTRACT: None -> Output[List[Dict]]
# @RELATION: [CALLS] ->[self.get_dashboards]
def get_dashboards_summary(self, require_slug: bool = False) -> List[Dict]:
with belief_scope("SupersetClient.get_dashboards_summary"):
# Rely on list endpoint default projection to stay compatible
@@ -229,12 +240,12 @@ class SupersetClient:
# [/DEF:get_dashboards_summary:Function]
# [DEF:get_dashboards_summary_page:Function]
# @TIER: STANDARD
# @PURPOSE: Fetches one page of dashboard metadata optimized for the grid.
# @PARAM: page (int) - 1-based page number from API route contract.
# @PARAM: page_size (int) - Number of items per page.
# @PRE: page >= 1 and page_size > 0.
# @POST: Returns mapped summaries and total dashboard count.
# @RETURN: Tuple[int, List[Dict]]
# @DATA_CONTRACT: Input[page: int, page_size: int] -> Output[Tuple[int, List[Dict]]]
# @RELATION: [CALLS] ->[self.get_dashboards_page]
def get_dashboards_summary_page(
self,
page: int,
@@ -302,10 +313,11 @@ class SupersetClient:
# [/DEF:get_dashboards_summary_page:Function]
# [DEF:_extract_owner_labels:Function]
# @TIER: TRIVIAL
# @PURPOSE: Normalize dashboard owners payload to stable display labels.
# @PRE: owners payload can be scalar, object or list.
# @POST: Returns deduplicated non-empty owner labels preserving order.
# @RETURN: List[str]
# @DATA_CONTRACT: Input[Any] -> Output[List[str]]
def _extract_owner_labels(self, owners_payload: Any) -> List[str]:
if owners_payload is None:
return []
@@ -329,10 +341,11 @@ class SupersetClient:
# [/DEF:_extract_owner_labels:Function]
# [DEF:_extract_user_display:Function]
# @TIER: TRIVIAL
# @PURPOSE: Normalize user payload to a stable display name.
# @PRE: user payload can be string, dict or None.
# @POST: Returns compact non-empty display value or None.
# @RETURN: Optional[str]
# @DATA_CONTRACT: Input[Optional[str], Optional[Dict]] -> Output[Optional[str]]
def _extract_user_display(self, preferred_value: Optional[str], user_payload: Optional[Dict]) -> Optional[str]:
preferred = self._sanitize_user_text(preferred_value)
if preferred:
@@ -357,10 +370,10 @@ class SupersetClient:
# [/DEF:_extract_user_display:Function]
# [DEF:_sanitize_user_text:Function]
# @TIER: TRIVIAL
# @PURPOSE: Convert scalar value to non-empty user-facing text.
# @PRE: value can be any scalar type.
# @POST: Returns trimmed string or None.
# @RETURN: Optional[str]
def _sanitize_user_text(self, value: Optional[Union[str, int]]) -> Optional[str]:
if value is None:
return None
@@ -371,10 +384,12 @@ class SupersetClient:
# [/DEF:_sanitize_user_text:Function]
# [DEF:get_dashboard:Function]
# @TIER: STANDARD
# @PURPOSE: Fetches a single dashboard by ID.
# @PRE: Client is authenticated and dashboard_id exists.
# @POST: Returns dashboard payload from Superset API.
# @RETURN: Dict
# @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]
# @RELATION: [CALLS] ->[self.network.request]
def get_dashboard(self, dashboard_id: int) -> Dict:
with belief_scope("SupersetClient.get_dashboard", f"id={dashboard_id}"):
response = self.network.request(method="GET", endpoint=f"/dashboard/{dashboard_id}")
@@ -382,10 +397,12 @@ class SupersetClient:
# [/DEF:get_dashboard:Function]
# [DEF:get_chart:Function]
# @TIER: STANDARD
# @PURPOSE: Fetches a single chart by ID.
# @PRE: Client is authenticated and chart_id exists.
# @POST: Returns chart payload from Superset API.
# @RETURN: Dict
# @DATA_CONTRACT: Input[chart_id: int] -> Output[Dict]
# @RELATION: [CALLS] ->[self.network.request]
def get_chart(self, chart_id: int) -> Dict:
with belief_scope("SupersetClient.get_chart", f"id={chart_id}"):
response = self.network.request(method="GET", endpoint=f"/chart/{chart_id}")
@@ -393,10 +410,13 @@ class SupersetClient:
# [/DEF:get_chart:Function]
# [DEF:get_dashboard_detail:Function]
# @TIER: STANDARD
# @PURPOSE: Fetches detailed dashboard information including related charts and datasets.
# @PRE: Client is authenticated and dashboard_id exists.
# @POST: Returns dashboard metadata with charts and datasets lists.
# @RETURN: Dict
# @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]
# @RELATION: [CALLS] ->[self.get_dashboard]
# @RELATION: [CALLS] ->[self.get_chart]
def get_dashboard_detail(self, dashboard_id: int) -> Dict:
with belief_scope("SupersetClient.get_dashboard_detail", f"id={dashboard_id}"):
dashboard_response = self.get_dashboard(dashboard_id)
@@ -585,11 +605,12 @@ class SupersetClient:
# [/DEF:get_dashboard_detail:Function]
# [DEF:get_charts:Function]
# @TIER: STANDARD
# @PURPOSE: Fetches all charts with pagination support.
# @PARAM: query (Optional[Dict]) - Optional query params/columns/filters.
# @PRE: Client is authenticated.
# @POST: Returns total count and charts list.
# @RETURN: Tuple[int, List[Dict]]
# @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
# @RELATION: [CALLS] ->[self._fetch_all_pages]
def get_charts(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
with belief_scope("get_charts"):
validated_query = self._validate_query_params(query or {})
@@ -636,11 +657,13 @@ class SupersetClient:
# [/DEF:_extract_chart_ids_from_layout:Function]
# [DEF:export_dashboard:Function]
# @TIER: STANDARD
# @PURPOSE: Экспортирует дашборд в виде ZIP-архива.
# @PARAM: dashboard_id (int) - ID дашборда для экспорта.
# @PRE: dashboard_id must exist in Superset.
# @POST: Returns ZIP content and filename.
# @RETURN: Tuple[bytes, str] - Бинарное содержимое ZIP-архива и имя файла.
# @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Tuple[bytes, str]]
# @SIDE_EFFECT: Performs network I/O to download archive.
# @RELATION: [CALLS] ->[self.network.request]
def export_dashboard(self, dashboard_id: int) -> Tuple[bytes, str]:
with belief_scope("export_dashboard"):
app_logger.info("[export_dashboard][Enter] Exporting dashboard %s.", dashboard_id)
@@ -659,13 +682,14 @@ class SupersetClient:
# [/DEF:export_dashboard:Function]
# [DEF:import_dashboard:Function]
# @TIER: STANDARD
# @PURPOSE: Импортирует дашборд из ZIP-файла.
# @PARAM: file_name (Union[str, Path]) - Путь к ZIP-архиву.
# @PARAM: dash_id (Optional[int]) - ID дашборда для удаления при сбое.
# @PARAM: dash_slug (Optional[str]) - Slug дашборда для поиска ID.
# @PRE: file_name must be a valid ZIP dashboard export.
# @POST: Dashboard is imported or re-imported after deletion.
# @RETURN: Dict - Ответ API в случае успеха.
# @DATA_CONTRACT: Input[file_name: Union[str, Path]] -> Output[Dict]
# @SIDE_EFFECT: Performs network I/O to upload archive.
# @RELATION: [CALLS] ->[self._do_import]
# @RELATION: [CALLS] ->[self.delete_dashboard]
def import_dashboard(self, file_name: Union[str, Path], dash_id: Optional[int] = None, dash_slug: Optional[str] = None) -> Dict:
with belief_scope("import_dashboard"):
if file_name is None:
@@ -690,10 +714,12 @@ class SupersetClient:
# [/DEF:import_dashboard:Function]
# [DEF:delete_dashboard:Function]
# @TIER: STANDARD
# @PURPOSE: Удаляет дашборд по его ID или slug.
# @PARAM: dashboard_id (Union[int, str]) - ID или slug дашборда.
# @PRE: dashboard_id must exist.
# @POST: Dashboard is removed from Superset.
# @SIDE_EFFECT: Deletes resource from upstream Superset environment.
# @RELATION: [CALLS] ->[self.network.request]
def delete_dashboard(self, dashboard_id: Union[int, str]) -> None:
with belief_scope("delete_dashboard"):
app_logger.info("[delete_dashboard][Enter] Deleting dashboard %s.", dashboard_id)
@@ -710,11 +736,12 @@ class SupersetClient:
# [SECTION: DATASET OPERATIONS]
# [DEF:get_datasets:Function]
# @TIER: STANDARD
# @PURPOSE: Получает полный список датасетов, автоматически обрабатывая пагинацию.
# @PARAM: query (Optional[Dict]) - Дополнительные параметры запроса.
# @PRE: Client is authenticated.
# @POST: Returns total count and list of datasets.
# @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список датасетов).
# @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
# @RELATION: [CALLS] ->[self._fetch_all_pages]
def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
with belief_scope("get_datasets"):
app_logger.info("[get_datasets][Enter] Fetching datasets.")
@@ -866,11 +893,12 @@ class SupersetClient:
# [/DEF:get_dataset_detail:Function]
# [DEF:get_dataset:Function]
# @TIER: STANDARD
# @PURPOSE: Получает информацию о конкретном датасете по его ID.
# @PARAM: dataset_id (int) - ID датасета.
# @PRE: dataset_id must exist.
# @POST: Returns dataset details.
# @RETURN: Dict - Информация о датасете.
# @DATA_CONTRACT: Input[dataset_id: int] -> Output[Dict]
# @RELATION: [CALLS] ->[self.network.request]
def get_dataset(self, dataset_id: int) -> Dict:
with belief_scope("SupersetClient.get_dataset", f"id={dataset_id}"):
app_logger.info("[get_dataset][Enter] Fetching dataset %s.", dataset_id)
@@ -881,12 +909,13 @@ class SupersetClient:
# [/DEF:get_dataset:Function]
# [DEF:update_dataset:Function]
# @TIER: STANDARD
# @PURPOSE: Обновляет данные датасета по его ID.
# @PARAM: dataset_id (int) - ID датасета.
# @PARAM: data (Dict) - Данные для обновления.
# @PRE: dataset_id must exist.
# @POST: Dataset is updated in Superset.
# @RETURN: Dict - Ответ API.
# @DATA_CONTRACT: Input[dataset_id: int, data: Dict] -> Output[Dict]
# @SIDE_EFFECT: Modifies resource in upstream Superset environment.
# @RELATION: [CALLS] ->[self.network.request]
def update_dataset(self, dataset_id: int, data: Dict) -> Dict:
with belief_scope("SupersetClient.update_dataset", f"id={dataset_id}"):
app_logger.info("[update_dataset][Enter] Updating dataset %s.", dataset_id)
@@ -906,11 +935,12 @@ class SupersetClient:
# [SECTION: DATABASE OPERATIONS]
# [DEF:get_databases:Function]
# @TIER: STANDARD
# @PURPOSE: Получает полный список баз данных.
# @PARAM: query (Optional[Dict]) - Дополнительные параметры запроса.
# @PRE: Client is authenticated.
# @POST: Returns total count and list of databases.
# @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список баз данных).
# @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
# @RELATION: [CALLS] ->[self._fetch_all_pages]
def get_databases(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
with belief_scope("get_databases"):
app_logger.info("[get_databases][Enter] Fetching databases.")
@@ -928,11 +958,12 @@ class SupersetClient:
# [/DEF:get_databases:Function]
# [DEF:get_database:Function]
# @TIER: STANDARD
# @PURPOSE: Получает информацию о конкретной базе данных по её ID.
# @PARAM: database_id (int) - ID базы данных.
# @PRE: database_id must exist.
# @POST: Returns database details.
# @RETURN: Dict - Информация о базе данных.
# @DATA_CONTRACT: Input[database_id: int] -> Output[Dict]
# @RELATION: [CALLS] ->[self.network.request]
def get_database(self, database_id: int) -> Dict:
with belief_scope("get_database"):
app_logger.info("[get_database][Enter] Fetching database %s.", database_id)
@@ -943,10 +974,12 @@ class SupersetClient:
# [/DEF:get_database:Function]
# [DEF:get_databases_summary:Function]
# @TIER: STANDARD
# @PURPOSE: Fetch a summary of databases including uuid, name, and engine.
# @PRE: Client is authenticated.
# @POST: Returns list of database summaries.
# @RETURN: List[Dict] - Summary of databases.
# @DATA_CONTRACT: None -> Output[List[Dict]]
# @RELATION: [CALLS] ->[self.get_databases]
def get_databases_summary(self) -> List[Dict]:
with belief_scope("SupersetClient.get_databases_summary"):
query = {
@@ -962,11 +995,12 @@ class SupersetClient:
# [/DEF:get_databases_summary:Function]
# [DEF:get_database_by_uuid:Function]
# @TIER: STANDARD
# @PURPOSE: Find a database by its UUID.
# @PARAM: db_uuid (str) - The UUID of the database.
# @PRE: db_uuid must be a valid UUID string.
# @POST: Returns database info or None.
# @RETURN: Optional[Dict] - Database info if found, else None.
# @DATA_CONTRACT: Input[db_uuid: str] -> Output[Optional[Dict]]
# @RELATION: [CALLS] ->[self.get_databases]
def get_database_by_uuid(self, db_uuid: str) -> Optional[Dict]:
with belief_scope("SupersetClient.get_database_by_uuid", f"uuid={db_uuid}"):
query = {
@@ -981,9 +1015,11 @@ class SupersetClient:
# [SECTION: HELPERS]
# [DEF:_resolve_target_id_for_delete:Function]
# @TIER: TRIVIAL
# @PURPOSE: Resolves a dashboard ID from either an ID or a slug.
# @PRE: Either dash_id or dash_slug should be provided.
# @POST: Returns the resolved ID or None.
# @RELATION: [CALLS] ->[self.get_dashboards]
def _resolve_target_id_for_delete(self, dash_id: Optional[int], dash_slug: Optional[str]) -> Optional[int]:
with belief_scope("_resolve_target_id_for_delete"):
if dash_id is not None:
@@ -1002,9 +1038,11 @@ class SupersetClient:
# [/DEF:_resolve_target_id_for_delete:Function]
# [DEF:_do_import:Function]
# @TIER: TRIVIAL
# @PURPOSE: Performs the actual multipart upload for import.
# @PRE: file_name must be a path to an existing ZIP file.
# @POST: Returns the API response from the upload.
# @RELATION: [CALLS] ->[self.network.upload_file]
def _do_import(self, file_name: Union[str, Path]) -> Dict:
with belief_scope("_do_import"):
app_logger.debug(f"[_do_import][State] Uploading file: {file_name}")
@@ -1022,6 +1060,7 @@ class SupersetClient:
# [/DEF:_do_import:Function]
# [DEF:_validate_export_response:Function]
# @TIER: TRIVIAL
# @PURPOSE: Validates that the export response is a non-empty ZIP archive.
# @PRE: response must be a valid requests.Response object.
# @POST: Raises SupersetAPIError if validation fails.
@@ -1035,6 +1074,7 @@ class SupersetClient:
# [/DEF:_validate_export_response:Function]
# [DEF:_resolve_export_filename:Function]
# @TIER: TRIVIAL
# @PURPOSE: Determines the filename for an exported dashboard.
# @PRE: response must contain Content-Disposition header or dashboard_id must be provided.
# @POST: Returns a sanitized filename string.
@@ -1050,6 +1090,7 @@ class SupersetClient:
# [/DEF:_resolve_export_filename:Function]
# [DEF:_validate_query_params:Function]
# @TIER: TRIVIAL
# @PURPOSE: Ensures query parameters have default page and page_size.
# @PRE: query can be None or a dictionary.
# @POST: Returns a dictionary with at least page and page_size.
@@ -1062,9 +1103,11 @@ class SupersetClient:
# [/DEF:_validate_query_params:Function]
# [DEF:_fetch_total_object_count:Function]
# @TIER: TRIVIAL
# @PURPOSE: Fetches the total number of items for a given endpoint.
# @PRE: endpoint must be a valid Superset API path.
# @POST: Returns the total count as an integer.
# @RELATION: [CALLS] ->[self.network.fetch_paginated_count]
def _fetch_total_object_count(self, endpoint: str) -> int:
with belief_scope("_fetch_total_object_count"):
return self.network.fetch_paginated_count(