001-fix-ui-ws-validation #2

Merged
busya merged 26 commits from 001-fix-ui-ws-validation into migration 2025-12-21 00:29:20 +03:00
26 changed files with 33396 additions and 2213 deletions
Showing only changes of commit e6346612c4 - Show all commits

View File

@@ -1,150 +1,206 @@
# <GRACE_MODULE id="search_script" name="search_script.py"> # <GRACE_MODULE id="search_script" name="search_script.py">
# @SEMANTICS: search, superset, dataset, regex # @SEMANTICS: search, superset, dataset, regex, file_output
# @PURPOSE: Предоставляет утилиты для поиска по текстовым паттернам в метаданных датасетов Superset. # @PURPOSE: Предоставляет утилиты для поиска по текстовым паттернам в метаданных датасетов Superset.
# @DEPENDS_ON: superset_tool.client -> Для взаимодействия с API Superset. # @DEPENDS_ON: superset_tool.client -> Для взаимодействия с API Superset.
# @DEPENDS_ON: superset_tool.utils -> Для логирования и инициализации клиентов. # @DEPENDS_ON: superset_tool.utils -> Для логирования и инициализации клиентов.
# <IMPORTS> # <IMPORTS>
import logging import logging
import re import re
from typing import Dict, Optional import os
from requests.exceptions import RequestException from typing import Dict, Optional
from superset_tool.client import SupersetClient from requests.exceptions import RequestException
from superset_tool.exceptions import SupersetAPIError from superset_tool.client import SupersetClient
from superset_tool.utils.logger import SupersetLogger from superset_tool.exceptions import SupersetAPIError
from superset_tool.utils.init_clients import setup_clients from superset_tool.utils.logger import SupersetLogger
# </IMPORTS> from superset_tool.utils.init_clients import setup_clients
# </IMPORTS>
# --- Начало кода модуля ---
# --- Начало кода модуля ---
# <ANCHOR id="search_datasets" type="Function">
# @PURPOSE: Выполняет поиск по строковому паттерну в метаданных всех датасетов. # <ANCHOR id="search_datasets" type="Function">
# @PRE: `client` должен быть инициализированным экземпляром `SupersetClient`. # @PURPOSE: Выполняет поиск по строковому паттерну в метаданных всех датасетов.
# @PRE: `search_pattern` должен быть валидной строкой регулярного выражения. # @PRE: `client` должен быть инициализированным экземпляром `SupersetClient`.
# @POST: Возвращает словарь с результатами поиска, где ключ - ID датасета, значение - список совпадений. # @PRE: `search_pattern` должен быть валидной строкой регулярного выражения.
# @PARAM: client: SupersetClient - Клиент для доступа к API Superset. # @POST: Возвращает словарь с результатами поиска, где ключ - ID датасета, значение - список совпадений.
# @PARAM: search_pattern: str - Регулярное выражение для поиска. # @PARAM: client: SupersetClient - Клиент для доступа к API Superset.
# @PARAM: logger: Optional[SupersetLogger] - Инстанс логгера. # @PARAM: search_pattern: str - Регулярное выражение для поиска.
# @RETURN: Optional[Dict] - Словарь с результатами или None, если ничего не найдено. # @PARAM: logger: Optional[SupersetLogger] - Инстанс логгера.
# @THROW: re.error - Если паттерн регулярного выражения невалиден. # @RETURN: Optional[Dict] - Словарь с результатами или None, если ничего не найдено.
# @THROW: SupersetAPIError, RequestException - При критических ошибках API. # @THROW: re.error - Если паттерн регулярного выражения невалиден.
# @RELATION: CALLS -> client.get_datasets # @THROW: SupersetAPIError, RequestException - При критических ошибках API.
def search_datasets( # @RELATION: CALLS -> client.get_datasets
client: SupersetClient, def search_datasets(
search_pattern: str, client: SupersetClient,
logger: Optional[SupersetLogger] = None search_pattern: str,
) -> Optional[Dict]: logger: Optional[SupersetLogger] = None
logger = logger or SupersetLogger(name="dataset_search") ) -> Optional[Dict]:
logger.info(f"[search_datasets][Enter] Searching for pattern: '{search_pattern}'") logger = logger or SupersetLogger(name="dataset_search")
try: logger.info(f"[search_datasets][Enter] Searching for pattern: '{search_pattern}'")
_, datasets = client.get_datasets(query={"columns": ["id", "table_name", "sql", "database", "columns"]}) try:
_, datasets = client.get_datasets(query={"columns": ["id", "table_name", "sql", "database", "columns"]})
if not datasets:
logger.warning("[search_datasets][State] No datasets found.") if not datasets:
return None logger.warning("[search_datasets][State] No datasets found.")
return None
pattern = re.compile(search_pattern, re.IGNORECASE)
results = {} pattern = re.compile(search_pattern, re.IGNORECASE)
results = {}
for dataset in datasets:
dataset_id = dataset.get('id') for dataset in datasets:
if not dataset_id: dataset_id = dataset.get('id')
continue if not dataset_id:
continue
matches = []
for field, value in dataset.items(): matches = []
value_str = str(value) for field, value in dataset.items():
if pattern.search(value_str): value_str = str(value)
match_obj = pattern.search(value_str) if pattern.search(value_str):
matches.append({ match_obj = pattern.search(value_str)
"field": field, matches.append({
"match": match_obj.group() if match_obj else "", "field": field,
"value": value_str "match": match_obj.group() if match_obj else "",
}) "value": value_str
})
if matches:
results[dataset_id] = matches if matches:
results[dataset_id] = matches
logger.info(f"[search_datasets][Success] Found matches in {len(results)} datasets.")
return results logger.info(f"[search_datasets][Success] Found matches in {len(results)} datasets.")
return results
except re.error as e:
logger.error(f"[search_datasets][Failure] Invalid regex pattern: {e}", exc_info=True) except re.error as e:
raise logger.error(f"[search_datasets][Failure] Invalid regex pattern: {e}", exc_info=True)
except (SupersetAPIError, RequestException) as e: raise
logger.critical(f"[search_datasets][Failure] Critical error during search: {e}", exc_info=True) except (SupersetAPIError, RequestException) as e:
raise logger.critical(f"[search_datasets][Failure] Critical error during search: {e}", exc_info=True)
# </ANCHOR id="search_datasets"> raise
# </ANCHOR id="search_datasets">
# <ANCHOR id="print_search_results" type="Function">
# @PURPOSE: Форматирует результаты поиска для читаемого вывода в консоль. # <ANCHOR id="save_results_to_file" type="Function">
# @PRE: `results` является словарем, возвращенным `search_datasets`, или `None`. # @PURPOSE: Сохраняет результаты поиска в текстовый файл.
# @POST: Возвращает отформатированную строку с результатами. # @PRE: `results` является словарем, возвращенным `search_datasets`, или `None`.
# @PARAM: results: Optional[Dict] - Словарь с результатами поиска. # @PRE: `filename` должен быть допустимым путем к файлу.
# @PARAM: context_lines: int - Количество строк контекста для вывода до и после совпадения. # @POST: Записывает отформатированные результаты в указанный файл.
# @RETURN: str - Отформатированный отчет. # @PARAM: results: Optional[Dict] - Словарь с результатами поиска.
def print_search_results(results: Optional[Dict], context_lines: int = 3) -> str: # @PARAM: filename: str - Имя файла для сохранения результатов.
if not results: # @PARAM: logger: Optional[SupersetLogger] - Инстанс логгера.
return "Ничего не найдено" # @RETURN: bool - Успешно ли выполнено сохранение.
def save_results_to_file(results: Optional[Dict], filename: str, logger: Optional[SupersetLogger] = None) -> bool:
output = [] logger = logger or SupersetLogger(name="file_writer")
for dataset_id, matches in results.items(): logger.info(f"[save_results_to_file][Enter] Saving results to file: {filename}")
output.append(f"\n--- Dataset ID: {dataset_id} ---") try:
formatted_report = print_search_results(results)
with open(filename, 'w', encoding='utf-8') as f:
f.write(formatted_report)
logger.info(f"[save_results_to_file][Success] Results saved to {filename}")
return True
except Exception as e:
logger.error(f"[save_results_to_file][Failure] Failed to save results to file: {e}", exc_info=True)
return False
# </ANCHOR id="save_results_to_file">
# <ANCHOR id="print_search_results" type="Function">
# @PURPOSE: Форматирует результаты поиска для читаемого вывода в консоль.
# @PRE: `results` является словарем, возвращенным `search_datasets`, или `None`.
# @POST: Возвращает отформатированную строку с результатами.
# @PARAM: results: Optional[Dict] - Словарь с результатами поиска.
# @PARAM: context_lines: int - Количество строк контекста для вывода до и после совпадения.
# @RETURN: str - Отформатированный отчет.
def print_search_results(results: Optional[Dict], context_lines: int = 3) -> str:
if not results:
return "Ничего не найдено"
output = []
for dataset_id, matches in results.items():
# Получаем информацию о базе данных для текущего датасета
database_info = ""
# Ищем поле database среди совпадений, чтобы вывести его
for match_info in matches: for match_info in matches:
field, match_text, full_value = match_info['field'], match_info['match'], match_info['value'] if match_info['field'] == 'database':
output.append(f" - Поле: {field}") database_info = match_info['value']
output.append(f" Совпадение: '{match_text}'") break
# Если database не найден в совпадениях, пробуем получить из других полей
if not database_info:
# Предполагаем, что база данных может быть в одном из полей, например sql или table_name
# Но для точности лучше использовать специальное поле, которое мы уже получили
pass # Пока не выводим, если не нашли явно
lines = full_value.splitlines() output.append(f"\n--- Dataset ID: {dataset_id} ---")
if not lines: continue if database_info:
output.append(f" Database: {database_info}")
output.append("") # Пустая строка для читабельности
for match_info in matches:
field, match_text, full_value = match_info['field'], match_info['match'], match_info['value']
output.append(f" - Поле: {field}")
output.append(f" Совпадение: '{match_text}'")
lines = full_value.splitlines()
if not lines: continue
match_line_index = -1
for i, line in enumerate(lines):
if match_text in line:
match_line_index = i
break
if match_line_index != -1:
start = max(0, match_line_index - context_lines)
end = min(len(lines), match_line_index + context_lines + 1)
output.append(" Контекст:")
for i in range(start, end):
prefix = f"{i + 1:5d}: "
line_content = lines[i]
if i == match_line_index:
highlighted = line_content.replace(match_text, f">>>{match_text}<<<")
output.append(f" {prefix}{highlighted}")
else:
output.append(f" {prefix}{line_content}")
output.append("-" * 25)
return "\n".join(output)
# </ANCHOR id="print_search_results">
# <ANCHOR id="main" type="Function">
# @PURPOSE: Основная точка входа для запуска скрипта поиска.
# @RELATION: CALLS -> setup_clients
# @RELATION: CALLS -> search_datasets
# @RELATION: CALLS -> print_search_results
# @RELATION: CALLS -> save_results_to_file
def main():
logger = SupersetLogger(level=logging.INFO, console=True)
clients = setup_clients(logger)
target_client = clients['prod']
search_query = r"from dm_view.[a-z_]*"
match_line_index = -1 # Генерируем имя файла на основе времени
for i, line in enumerate(lines): import datetime
if match_text in line: timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
match_line_index = i output_filename = f"search_results_{timestamp}.txt"
break
results = search_datasets(
if match_line_index != -1: client=target_client,
start = max(0, match_line_index - context_lines) search_pattern=search_query,
end = min(len(lines), match_line_index + context_lines + 1) logger=logger
output.append(" Контекст:") )
for i in range(start, end):
prefix = f"{i + 1:5d}: " report = print_search_results(results)
line_content = lines[i]
if i == match_line_index: logger.info(f"[main][Success] Search finished. Report:\n{report}")
highlighted = line_content.replace(match_text, f">>>{match_text}<<<")
output.append(f" {prefix}{highlighted}") # Сохраняем результаты в файл
else: success = save_results_to_file(results, output_filename, logger)
output.append(f" {prefix}{line_content}") if success:
output.append("-" * 25) logger.info(f"[main][Success] Results also saved to file: {output_filename}")
return "\n".join(output) else:
# </ANCHOR id="print_search_results"> logger.error(f"[main][Failure] Failed to save results to file: {output_filename}")
# <ANCHOR id="main" type="Function"> # </ANCHOR id="main">
# @PURPOSE: Основная точка входа для запуска скрипта поиска.
# @RELATION: CALLS -> setup_clients if __name__ == "__main__":
# @RELATION: CALLS -> search_datasets main()
# @RELATION: CALLS -> print_search_results
def main(): # --- Конец кода модуля ---
logger = SupersetLogger(level=logging.INFO, console=True)
clients = setup_clients(logger) # </GRACE_MODULE id="search_script">
target_client = clients['prod']
search_query = r".account_balance_by_contract"
results = search_datasets(
client=target_client,
search_pattern=search_query,
logger=logger
)
report = print_search_results(results)
logger.info(f"[main][Success] Search finished. Report:\n{report}")
# </ANCHOR id="main">
if __name__ == "__main__":
main()
# --- Конец кода модуля ---
# </GRACE_MODULE id="search_script">