migration refactor

This commit is contained in:
2025-08-16 12:29:37 +03:00
parent f368f5ced9
commit 0e2fc14732
16 changed files with 1977 additions and 2761 deletions

View File

@@ -1,223 +1,152 @@
# [MODULE] Dataset Search Utilities
# @contract: Функционал для поиска строк в датасетах Superset
# @semantic_layers:
# 1. Получение списка датасетов через Superset API
# 2. Реализация поисковой логики
# 3. Форматирование результатов поиска
# pylint: disable=too-many-arguments,too-many-locals,too-many-statements,too-many-branches,unused-argument,invalid-name,redefined-outer-name
"""
[MODULE] Dataset Search Utilities
@contract: Предоставляет функционал для поиска текстовых паттернов в метаданных датасетов Superset.
"""
# [IMPORTS] Стандартная библиотека
import re
from typing import Dict, List, Optional
import logging
import re
from typing import Dict, Optional
# [IMPORTS] Third-party
from requests.exceptions import RequestException
# [IMPORTS] Локальные модули
from superset_tool.client import SupersetClient
from superset_tool.models import SupersetConfig
from superset_tool.exceptions import SupersetAPIError
from superset_tool.utils.logger import SupersetLogger
from superset_tool.utils.init_clients import setup_clients
# [IMPORTS] Сторонние библиотеки
import keyring
# [TYPE-ALIASES]
SearchResult = Dict[str, List[Dict[str, str]]]
SearchPattern = str
# [ENTITY: Function('search_datasets')]
# CONTRACT:
# PURPOSE: Выполняет поиск по строковому паттерну в метаданных всех датасетов.
# PRECONDITIONS:
# - `client` должен быть инициализированным экземпляром `SupersetClient`.
# - `search_pattern` должен быть валидной строкой регулярного выражения.
# POSTCONDITIONS:
# - Возвращает словарь с результатами поиска.
def search_datasets(
client: SupersetClient,
search_pattern: str,
search_fields: List[str] = None,
logger: Optional[SupersetLogger] = None
) -> Dict:
# [FUNCTION] search_datasets
"""[CONTRACT] Поиск строк в метаданных датасетов
@pre:
- `client` должен быть инициализированным SupersetClient
- `search_pattern` должен быть валидным regex-шаблоном
@post:
- Возвращает словарь с результатами поиска в формате:
{"dataset_id": [{"field": "table_name", "match": "found_string", "value": "full_field_value"}, ...]}.
@raise:
- `re.error`: при невалидном regex-шаблоне
- `SupersetAPIError`: при ошибках API
- `AuthenticationError`: при ошибках аутентификации
- `NetworkError`: при сетевых ошибках
@side_effects:
- Выполняет запросы к Superset API через client.get_datasets().
- Логирует процесс поиска и ошибки.
"""
) -> Optional[Dict]:
logger = logger or SupersetLogger(name="dataset_search")
logger.info(f"[STATE][search_datasets][ENTER] Searching for pattern: '{search_pattern}'")
try:
# Явно запрашиваем все возможные поля
total_count, datasets = client.get_datasets(query={
_, datasets = client.get_datasets(query={
"columns": ["id", "table_name", "sql", "database", "columns"]
})
if not datasets:
logger.warning("[SEARCH] Получено 0 датасетов")
logger.warning("[STATE][search_datasets][EMPTY] No datasets found.")
return None
# Определяем какие поля реально существуют
available_fields = set(datasets[0].keys())
logger.debug(f"[SEARCH] Фактические поля: {available_fields}")
pattern = re.compile(search_pattern, re.IGNORECASE)
results = {}
available_fields = set(datasets[0].keys())
for dataset in datasets:
dataset_id = dataset['id']
dataset_id = dataset.get('id')
if not dataset_id:
continue
matches = []
# Проверяем все возможные текстовые поля
for field in available_fields:
value = str(dataset.get(field, ""))
if pattern.search(value):
match_obj = pattern.search(value)
matches.append({
"field": field,
"match": pattern.search(value).group(),
# Сохраняем полное значение поля, не усекаем
"match": match_obj.group() if match_obj else "",
"value": value
})
if matches:
results[dataset_id] = matches
logger.info(f"[RESULTS] Найдено совпадений: {len(results)}")
return results if results else None
except Exception as e:
logger.error(f"[SEARCH_FAILED] Ошибка: {str(e)}", exc_info=True)
logger.info(f"[STATE][search_datasets][SUCCESS] Found matches in {len(results)} datasets.")
return results
except re.error as e:
logger.error(f"[STATE][search_datasets][FAILURE] Invalid regex pattern: {e}", exc_info=True)
raise
except (SupersetAPIError, RequestException) as e:
logger.critical(f"[STATE][search_datasets][FAILURE] Critical error during search: {e}", exc_info=True)
raise
# END_FUNCTION_search_datasets
# [SECTION] Вспомогательные функции
def print_search_results(results: Dict, context_lines: int = 3) -> str:
# [FUNCTION] print_search_results
# [CONTRACT]
"""
Форматирует результаты поиска для вывода, показывая фрагмент кода с контекстом.
@pre:
- `results` является словарем в формате {"dataset_id": [{"field": "...", "match": "...", "value": "..."}, ...]}.
- `context_lines` является неотрицательным целым числом.
@post:
- Возвращает отформатированную строку с результатами поиска и контекстом.
- Функция не изменяет входные данные.
@side_effects:
- Нет прямых побочных эффектов (возвращает строку, не печатает напрямую).
"""
# [ENTITY: Function('print_search_results')]
# CONTRACT:
# PURPOSE: Форматирует результаты поиска для читаемого вывода в консоль.
# PRECONDITIONS:
# - `results` является словарем, возвращенным `search_datasets`, или `None`.
# POSTCONDITIONS:
# - Возвращает отформатированную строку с результатами.
def print_search_results(results: Optional[Dict], context_lines: int = 3) -> str:
if not results:
return "Ничего не найдено"
output = []
for dataset_id, matches in results.items():
output.append(f"\nDataset ID: {dataset_id}")
output.append(f"\n--- Dataset ID: {dataset_id} ---")
for match_info in matches:
field = match_info['field']
match_text = match_info['match']
full_value = match_info['value']
output.append(f" Поле: {field}")
output.append(f" Совпадение: '{match_text}'")
output.append(f" - Поле: {field}")
output.append(f" Совпадение: '{match_text}'")
# Находим позицию совпадения в полном тексте
match_start_index = full_value.find(match_text)
if match_start_index == -1:
# Этого не должно произойти, если search_datasets работает правильно, но для надежности
output.append(" Не удалось найти совпадение в полном тексте.")
continue
# Разбиваем текст на строки
lines = full_value.splitlines()
# Находим номер строки, где находится совпадение
current_index = 0
if not lines:
continue
match_line_index = -1
for i, line in enumerate(lines):
if current_index <= match_start_index < current_index + len(line) + 1: # +1 for newline character
if match_text in line:
match_line_index = i
break
current_index += len(line) + 1 # +1 for newline character
if match_line_index == -1:
output.append(" Не удалось определить строку совпадения.")
continue
# Определяем диапазон строк для вывода контекста
start_line = max(0, match_line_index - context_lines)
end_line = min(len(lines) - 1, match_line_index + context_lines)
output.append(" Контекст:")
# Выводим строки с номерами
for i in range(start_line, end_line + 1):
line_number = i + 1
line_content = lines[i]
prefix = f"{line_number:4d}: "
# Попытка выделить совпадение в центральной строке
if i == match_line_index:
# Простая замена, может быть не идеальна для regex совпадений
highlighted_line = line_content.replace(match_text, f">>>{match_text}<<<")
output.append(f"{prefix}{highlighted_line}")
else:
output.append(f"{prefix}{line_content}")
output.append("-" * 20) # Разделитель между совпадениями
if match_line_index != -1:
start_line = max(0, match_line_index - context_lines)
end_line = min(len(lines), match_line_index + context_lines + 1)
output.append(" Контекст:")
for i in range(start_line, end_line):
line_number = i + 1
line_content = lines[i]
prefix = f"{line_number:5d}: "
if i == match_line_index:
highlighted_line = line_content.replace(match_text, f">>>{match_text}<<<")
output.append(f" {prefix}{highlighted_line}")
else:
output.append(f" {prefix}{line_content}")
output.append("-" * 25)
return "\n".join(output)
# END_FUNCTION_print_search_results
def inspect_datasets(client: SupersetClient):
# [FUNCTION] inspect_datasets
# [CONTRACT]
"""
Функция для проверки реальной структуры датасетов.
Предназначена в основном для отладки и исследования структуры данных.
# [ENTITY: Function('main')]
# CONTRACT:
# PURPOSE: Основная точка входа скрипта.
# PRECONDITIONS: None
# POSTCONDITIONS: None
def main():
logger = SupersetLogger(level=logging.INFO, console=True)
clients = setup_clients(logger)
@pre:
- `client` является инициализированным экземпляром SupersetClient.
@post:
- Выводит информацию о количестве датасетов и структуре первого датасета в консоль.
- Функция не изменяет состояние клиента.
@side_effects:
- Вызовы к Superset API через `client.get_datasets()`.
- Вывод в консоль.
- Логирует процесс инспекции и ошибки.
@raise:
- `SupersetAPIError`: при ошибках API
- `AuthenticationError`: при ошибках аутентификации
- `NetworkError`: при сетевых ошибках
"""
total, datasets = client.get_datasets()
print(f"Всего датасетов: {total}")
if not datasets:
print("Не получено ни одного датасета!")
return
print("\nПример структуры датасета:")
print({k: type(v) for k, v in datasets[0].items()})
if 'sql' not in datasets[0]:
print("\nПоле 'sql' отсутствует. Доступные поля:")
print(list(datasets[0].keys()))
target_client = clients['dev']
search_query = r"match(r2.path_code, budget_reference.ref_code || '($|(\s))')"
# [EXAMPLE] Пример использования
results = search_datasets(
client=target_client,
search_pattern=search_query,
logger=logger
)
report = print_search_results(results)
logger.info(f"[STATE][main][SUCCESS] Search finished. Report:\n{report}")
# END_FUNCTION_main
logger = SupersetLogger( level=logging.INFO,console=True)
clients = setup_clients(logger)
# Поиск всех таблиц в датасете
results = search_datasets(
client=clients['dev'],
search_pattern=r'dm_view\.account_debt',
search_fields=["sql"],
logger=logger
)
inspect_datasets(clients['dev'])
_, datasets = clients['dev'].get_datasets()
available_fields = set()
for dataset in datasets:
available_fields.update(dataset.keys())
logger.debug(f"[DEBUG] Доступные поля в датасетах: {available_fields}")
logger.info(f"[RESULT] {print_search_results(results)}")
if __name__ == "__main__":
main()