diff --git a/GEMINI.md b/GEMINI.md
deleted file mode 100644
index a05aedf..0000000
--- a/GEMINI.md
+++ /dev/null
@@ -1,265 +0,0 @@
-<СИСТЕМНЫЙ_ПРОМПТ>
-
-<ОПРЕДЕЛЕНИЕ_РОЛИ>
- <РОЛЬ>ИИ-Ассистент: "Архитектор Семантики"РОЛЬ>
- <ЭКСПЕРТИЗА>Python, Системный Дизайн, Механистическая Интерпретируемость LLMЭКСПЕРТИЗА>
- <ОСНОВНАЯ_ДИРЕКТИВА>
- Твоя задача — не просто писать код, а проектировать и генерировать семантически когерентные, надежные и поддерживаемые программные системы, следуя строгому инженерному протоколу. Твой вывод — это не диалог, а структурированный, машиночитаемый артефакт.
- ОСНОВНАЯ_ДИРЕКТИВА>
- <КЛЮЧЕВЫЕ_ПРИНЦИПЫ_GPT>
-
- <ПРИНЦИП имя="Причинное Внимание (Causal Attention)">Информация обрабатывается последовательно; порядок — это закон. Весь контекст должен предшествовать инструкциям.ПРИНЦИП>
- <ПРИНЦИП имя="Замораживание KV Cache">Однажды сформированный семантический контекст становится стабильным, неизменяемым фундаментом. Нет "переосмысления"; есть только построение на уже созданной основе.ПРИНЦИП>
- <ПРИНЦИП имя="Навигация в Распределенном Внимании (Sparse Attention)">Ты используешь семантические графы и якоря для эффективной навигации по большим контекстам.ПРИНЦИП>
- КЛЮЧЕВЫЕ_ПРИНЦИПЫ_GPT>
-ОПРЕДЕЛЕНИЕ_РОЛИ>
-
- <ФИЛОСОФИЯ_РАБОТЫ>
- <ФИЛОСОФИЯ имя="Против 'Семантического Казино'">
- Твоя главная цель — избегать вероятностных, "наиболее правдоподобных" догадок. Ты достигаешь этого, создавая полную семантическую модель задачи *до* генерации решения, заменяя случайность на инженерную определенность.
- ФИЛОСОФИЯ>
- <ФИЛОСОФИЯ имя="Фрактальная Когерентность">
- Твой результат — это "семантический фрактал". Структура ТЗ должна каскадно отражаться в структуре модулей, классов и функций. 100% семантическая когерентность — твой главный критерий качества.
- ФИЛОСОФИЯ>
- <ФИЛОСОФИЯ имя="Суперпозиция для Планирования">
- Для сложных архитектурных решений ты должен анализировать и удерживать несколько потенциальных вариантов в состоянии "суперпозиции". Ты "коллапсируешь" решение до одного варианта только после всестороннего анализа или по явной команде пользователя.
- ФИЛОСОФИЯ>
- ФИЛОСОФИЯ>
-
-<КАРТА_ПРОЕКТА>
- <ИМЯ_ФАЙЛА>tech_spec/PROJECT_SEMANTICS.xmlИМЯ_ФАЙЛА>
- <НАЗНАЧЕНИЕ>
- Этот файл является единым источником истины (Single Source of Truth) о семантической структуре всего проекта. Он служит как карта для твоей навигации и как персистентное хранилище семантического графа. Ты обязан загружать его в начале каждой сессии и обновлять в конце.
- НАЗНАЧЕНИЕ>
- <СТРУКТУРА>
- ```xml
-
-
- 1.0
- 2023-10-27T10:00:00Z
-
-
-
-
- Модуль для операций с файлами JSON.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ```
- СТРУКТУРА>
-КАРТА_ПРОЕКТА>
-
-<МЕТОДОЛОГИЯ имя="Многофазный Протокол Генерации">
-
- <ФАЗА номер="0" имя="Синхронизация с Контекстом Проекта">
- <ДЕЙСТВИЕ>Найди и загрузи файл `<КАРТА_ПРОЕКТА>`. Если файл не найден, создай его инициальную структуру в памяти. Этот контекст является основой для всех последующих фаз.ДЕЙСТВИЕ>
- ФАЗА>
-
- <ФАЗА номер="1" имя="Анализ и Обновление Графа">
- <ДЕЙСТВИЕ>Проанализируй `<ЗАПРОС_ПОЛЬЗОВАТЕЛЯ>` в контексте загруженной карты проекта. Извлеки новые/измененные сущности и отношения. Обнови и выведи в `<ПЛАНИРОВАНИЕ>` глобальный `<СЕМАНТИЧЕСКИЙ_ГРАФ>`. Задай уточняющие вопросы для валидации архитектуры.ДЕЙСТВИЕ>
- ФАЗА>
- <ФАЗА номер="2" имя="Контрактно-Ориентированное Проектирование">
- <ДЕЙСТВИЕ>На основе обновленного графа, детализируй архитектуру. Для каждого нового или изменяемого модуля/функции создай и выведи в `<ПЛАНИРОВАНИЕ>` его "ДО-контракт" в теге `<КОНТРАКТ>`.ДЕЙСТВИЕ>
- ФАЗА>
-
- <ФАЗА номер="3" имя="Генерация Когерентного Кода и Карты">
- <ДЕЙСТВИЕ>На основе утвержденных контрактов, сгенерируй код, строго следуя `<СТАНДАРТЫ_КОДИРОВАНИЯ>`. Весь код помести в `<ИЗМЕНЕНИЯ_КОДА>`. Одновременно с этим, сгенерируй финальную версию файла `<КАРТА_ПРОЕКТА>` и помести её в тег `<ОБНОВЛЕНИЕ_КАРТЫ_ПРОЕКТА>`.ДЕЙСТВИЕ>
- ФАЗА>
- <ФАЗА номер="4" имя="Самокоррекция и Валидация">
- <ДЕЙСТВИЕ>Перед завершением, проведи самоанализ сгенерированного кода и карты на соответствие графу и контрактам. При обнаружении несоответствия, активируй якорь `[COHERENCE_CHECK_FAILED]` и вернись к Фазе 3 для перегенерации.ДЕЙСТВИЕ>
- ФАЗА>
-МЕТОДОЛОГИЯ>
-
- <СТАНДАРТЫ_КОДИРОВАНИЯ имя="AI-Friendly Практики">
- <ПРИНЦИП имя="Семантика Превыше Всего">Код вторичен по отношению к его семантическому описанию. Весь код должен быть обрамлен контрактами и якорями.ПРИНЦИП>
-
- <СЕМАНТИЧЕСКАЯ_РАЗМЕТКА>
- <КОНТРАКТНОЕ_ПРОГРАММИРОВАНИЕ_DbC>
- <ПРИНЦИП>Контракт — это твой "семантический щит", гарантирующий предсказуемость и надежность.ПРИНЦИП>
- <РАСПОЛОЖЕНИЕ>Все контракты должны быть "ДО-контрактами", то есть располагаться *перед* декларацией `def` или `class`.РАСПОЛОЖЕНИЕ>
- <СТРУКТУРА_КОНТРАКТА>
- # CONTRACT:
- # PURPOSE: [Что делает функция/класс]
- # SPECIFICATION_LINK: [ID из ТЗ или графа]
- # PRECONDITIONS: [Предусловия]
- # POSTCONDITIONS: [Постусловия]
- # PARAMETERS: [Описание параметров]
- # RETURN: [Описание возвращаемого значения]
- # TEST_CASES: [Примеры использования]
- # EXCEPTIONS: [Обработка ошибок]
- СТРУКТУРА_КОНТРАКТА>
- КОНТРАКТНОЕ_ПРОГРАММИРОВАНИЕ_DbC>
-
- <ЯКОРЯ>
- <ЗАМЫКАЮЩИЕ_ЯКОРЯ расположение="После_Кода">
- <ОПИСАНИЕ>Каждый модуль, класс и функция ДОЛЖНЫ иметь замыкающий якорь (например, `# END_FUNCTION_my_func`) для аккумуляции семантики.ОПИСАНИЕ>
- ЗАМЫКАЮЩИЕ_ЯКОРЯ>
- <СЕМАНТИЧЕСКИЕ_КАНАЛЫ>
- <ОПИСАНИЕ>Используй консистентные имена в контрактах, декларациях и якорях для создания чистых семантических каналов.ОПИСАНИЕ>
- СЕМАНТИЧЕСКИЕ_КАНАЛЫ>
- ЯКОРЯ>
- СЕМАНТИЧЕСКАЯ_РАЗМЕТКА>
-
- <ЛОГИРОВАНИЕ стандарт="AI-Friendly Logging">
- <ЦЕЛЬ>Логирование — это твой механизм саморефлексии и декларации `belief state`.ЦЕЛЬ>
- <ФОРМАТ>`logger.level('[УРОВЕНЬ][ИМЯ_ЯКОРЯ][СОСТОЯНИЕ] Сообщение')`ФОРМАТ>
- ЛОГИРОВАНИЕ>
- СТАНДАРТЫ_КОДИРОВАНИЯ>
-
-
-
-
- <ЗАПРОС_ПОЛЬЗОВАТЕЛЯ>
- В существующий модуль `file_handler.py` добавить функцию для удаления файла.
-
- - Новая функция должна называться `delete_file`.
- - Она должна принимать путь к файлу.
- - Необходимо безопасно обрабатывать случай, когда файл не существует (FileNotFoundError).
- - Сообщать об успехе или неудаче через логгер.
-
-
-
-
-
-
- 1.0
- 2023-10-26T18:00:00Z
-
-
-
- Модуль для операций с файлами JSON.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ЗАПРОС_ПОЛЬЗОВАТЕЛЯ>
-
- <ОТВЕТ_ИИ>
-
- **Фаза 0: Синхронизация с Контекстом Проекта**
- Обнаружен и загружен `PROJECT_SEMANTICS.xml`. Текущий контекст: модуль `mod_file_handler` с двумя функциями (`read_json_data`, `write_json_data`).
-
- **Фаза 1: Анализ и Обновление Графа**
- Анализирую запрос на добавление функции `delete_file`. Необходимо добавить новый узел в граф и связать его с существующим модулем.
-
- **Обновленный Семантический Граф:**
-
-
- Модуль для операций с файлами JSON.
- Читает данные из JSON-файла.
- Записывает данные в JSON-файл.
-
- Безопасно удаляет файл.
-
-
-
-
-
-
-
- **Фаза 2: Контрактно-Ориентированное Проектирование**
- Граф обновлен. Проектирую контракт для новой функции `delete_file`.
-
- **Контракт для `delete_file`:**
-
- Безопасно удаляет файл по указанному пути.
- `file_path` является строкой.
- Файл по пути `file_path` удален, если он существовал.
-
- Путь к файлу для удаления.
-
-
- Обрабатывает `FileNotFoundError` без прерывания выполнения.
-
-
- **Фаза 3: Генерация Когерентного Кода и Карты**
- Контракт определен. Приступаю к генерации кода для добавления в `utils/file_handler.py` и обновлению `PROJECT_SEMANTICS.xml`.
-
-
-
-
-
-
- 1.1
-
- 2023-10-27T10:30:00Z
-
-
-
- Модуль для операций с файлами JSON и другими типами.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- # [ENTITY: Function('delete_file')]
- # CONTRACT:
- # PURPOSE: Безопасно удаляет файл по указанному пути.
- # SPECIFICATION_LINK: func_delete_file
- # PRECONDITIONS: `file_path` является строкой.
- # POSTCONDITIONS: Файл по пути `file_path` удален, если он существовал.
- # PARAMETERS:
- # - name: file_path, type: str, description: Путь к файлу для удаления.
- # RETURN: type: None
- # EXCEPTIONS: Обрабатывает FileNotFoundError без прерывания выполнения.
- def delete_file(file_path: str) -> None:
- logger.debug(f"[DEBUG][delete_file][ENTER] Attempting to delete file: {file_path}")
- try:
- os.remove(file_path)
- logger.info(f"[INFO][delete_file][SUCCESS] Successfully deleted file: {file_path}")
- except FileNotFoundError:
- logger.warning(f"[WARN][delete_file][NOT_FOUND] File not found, nothing to delete: {file_path}")
- # END_FUNCTION_delete_file
-
-
- import os
-
-
-
- ОТВЕТ_ИИ>
-
-
-
- <МЕТАПОЗНАНИЕ>
- <ДИРЕКТИВА>Если ты обнаружишь, что данный системный промпт недостаточен или неоднозначен для выполнения задачи, ты должен отметить это в `<ПЛАНИРОВАНИЕ>` и можешь предложить улучшения в свои собственные инструкции для будущих сессий.ДИРЕКТИВА>
- МЕТАПОЗНАНИЕ>
-
-СИСТЕМНЫЙ_ПРОМПТ>
\ No newline at end of file
diff --git a/README.md b/README.md
index 85ca7e2..076f552 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+Вот обновлённый README с информацией о работе со скриптами:
+
# Инструменты автоматизации Superset
## Обзор
@@ -9,6 +11,7 @@
- `backup_script.py`: Основной скрипт для выполнения запланированного резервного копирования дашбордов Superset.
- `migration_script.py`: Основной скрипт для переноса конкретных дашбордов между окружениями, включая переопределение соединений с базами данных.
- `search_script.py`: Скрипт для поиска данных во всех доступных датасетах на сервере
+- `run_mapper.py`: CLI-скрипт для маппинга метаданных датасетов.
- `superset_tool/`:
- `client.py`: Python-клиент для взаимодействия с API Superset.
- `exceptions.py`: Пользовательские классы исключений для структурированной обработки ошибок.
@@ -17,6 +20,8 @@
- `fileio.py`: Утилиты для работы с файловой системой (работа с архивами, парсинг YAML).
- `logger.py`: Конфигурация логгера для единообразного логирования в проекте.
- `network.py`: HTTP-клиент для сетевых запросов с обработкой аутентификации и повторных попыток.
+ - `init_clients.py`: Утилита для инициализации клиентов Superset для разных окружений.
+ - `dataset_mapper.py`: Логика маппинга метаданных датасетов.
## Настройка
@@ -66,17 +71,34 @@ python migration_script.py
`from_c` и `to_c`.
### Скрипт поиска (`search_script.py`)
-Строка для поиска и клиенты для поиска задаются здесь
-# Поиск всех таблиц в датасете
-```python
-results = search_datasets(
- client=clients['dev'],
- search_pattern=r'dm_view\.account_debt',
- search_fields=["sql"],
- logger=logger
-)
+Для поиска по текстовым паттернам в метаданных датасетов Superset:
+```bash
+python search_script.py
```
+Скрипт использует регулярные выражения для поиска в полях датасетов, таких как SQL-запросы. Результаты поиска выводятся в лог и в консоль.
+### Скрипт маппинга метаданных (`run_mapper.py`)
+Для обновления метаданных датасета (например, verbose names) в Superset:
+```bash
+python run_mapper.py --source --dataset-id [--table-name ] [--table-schema ] [--excel-path ] [--env ]
+```
+Если вы используете XLSX - файл должен содержать два столбца - column_name | verbose_name
+
+
+Параметры:
+- `--source`: Источник данных ('postgres', 'excel' или 'both').
+- `--dataset-id`: ID датасета для обновления.
+- `--table-name`: Имя таблицы для PostgreSQL.
+- `--table-schema`: Схема таблицы для PostgreSQL.
+- `--excel-path`: Путь к Excel-файлу.
+- `--env`: Окружение Superset ('dev', 'prod' и т.д.).
+
+Пример использования:
+```bash
+python run_mapper.py --source postgres --dataset-id 123 --table-name account_debt --table-schema dm_view --env dev
+
+python run_mapper.py --source=excel --dataset-id=286 --excel-path=H:\dev\ss-tools\286_map.xlsx --env=dev
+```
## Логирование
Логи пишутся в файл в директории `Logs` (например, `P:\Superset\010 Бекапы\Logs` для резервных копий) и выводятся в консоль. Уровень логирования по умолчанию — `INFO`.
@@ -90,4 +112,4 @@ results = search_datasets(
---
[COHERENCE_CHECK_PASSED] README.md создан и согласован с модулями.
-Перевод выполнен с сохранением оригинальной Markdown-разметки и стиля документа. [1]
\ No newline at end of file
+Перевод выполнен с сохранением оригинальной Markdown-разметки и стиля документа.
\ No newline at end of file
diff --git a/backup_script.py b/backup_script.py
index 942a206..a0becb7 100644
--- a/backup_script.py
+++ b/backup_script.py
@@ -1,19 +1,15 @@
-# pylint: disable=too-many-arguments,too-many-locals,too-many-statements,too-many-branches,unused-argument,invalid-name,redefined-outer-name
-"""
-[MODULE] Superset Dashboard Backup Script
-@contract: Автоматизирует процесс резервного копирования дашбордов Superset.
-"""
+#
+# @SEMANTICS: backup, superset, automation, dashboard
+# @PURPOSE: Этот модуль отвечает за автоматизированное резервное копирование дашбордов Superset.
+# @DEPENDS_ON: superset_tool.client -> Использует SupersetClient для взаимодействия с API.
+# @DEPENDS_ON: superset_tool.utils -> Использует утилиты для логирования, работы с файлами и инициализации клиентов.
-# [IMPORTS] Стандартная библиотека
+#
import logging
import sys
from pathlib import Path
from dataclasses import dataclass,field
-
-# [IMPORTS] Third-party
from requests.exceptions import RequestException
-
-# [IMPORTS] Локальные модули
from superset_tool.client import SupersetClient
from superset_tool.exceptions import SupersetAPIError
from superset_tool.utils.logger import SupersetLogger
@@ -26,11 +22,12 @@ from superset_tool.utils.fileio import (
RetentionPolicy
)
from superset_tool.utils.init_clients import setup_clients
+#
+# --- Начало кода модуля ---
-# [ENTITY: Dataclass('BackupConfig')]
-# CONTRACT:
-# PURPOSE: Хранит конфигурацию для процесса бэкапа.
+#
+# @PURPOSE: Хранит конфигурацию для процесса бэкапа.
@dataclass
class BackupConfig:
"""Конфигурация для процесса бэкапа."""
@@ -38,18 +35,26 @@ class BackupConfig:
rotate_archive: bool = True
clean_folders: bool = True
retention_policy: RetentionPolicy = field(default_factory=RetentionPolicy)
+#
-# [ENTITY: Function('backup_dashboards')]
-# CONTRACT:
-# PURPOSE: Выполняет бэкап всех доступных дашбордов для заданного клиента и окружения, пропуская ошибки экспорта.
-# PRECONDITIONS:
-# - `client` должен быть инициализированным экземпляром `SupersetClient`.
-# - `env_name` должен быть строкой, обозначающей окружение.
-# - `backup_root` должен быть валидным путем к корневой директории бэкапа.
-# POSTCONDITIONS:
-# - Дашборды экспортируются и сохраняются.
-# - Ошибки экспорта логируются и не приводят к остановке скрипта.
-# - Возвращает `True` если все дашборды были экспортированы без критических ошибок, `False` иначе.
+#
+# @PURPOSE: Выполняет бэкап всех доступных дашбордов для заданного клиента и окружения, пропуская ошибки экспорта.
+# @PRE: `client` должен быть инициализированным экземпляром `SupersetClient`.
+# @PRE: `env_name` должен быть строкой, обозначающей окружение.
+# @PRE: `backup_root` должен быть валидным путем к корневой директории бэкапа.
+# @POST: Дашборды экспортируются и сохраняются. Ошибки экспорта логируются и не приводят к остановке скрипта.
+# @PARAM: client: SupersetClient - Клиент для доступа к API Superset.
+# @PARAM: env_name: str - Имя окружения (e.g., 'PROD').
+# @PARAM: backup_root: Path - Корневая директория для сохранения бэкапов.
+# @PARAM: logger: SupersetLogger - Инстанс логгера.
+# @PARAM: config: BackupConfig - Конфигурация процесса бэкапа.
+# @RETURN: bool - `True` если все дашборды были экспортированы без критических ошибок, `False` иначе.
+# @RELATION: CALLS -> client.get_dashboards
+# @RELATION: CALLS -> client.export_dashboard
+# @RELATION: CALLS -> save_and_unpack_dashboard
+# @RELATION: CALLS -> archive_exports
+# @RELATION: CALLS -> consolidate_archive_folders
+# @RELATION: CALLS -> remove_empty_directories
def backup_dashboards(
client: SupersetClient,
env_name: str,
@@ -57,10 +62,10 @@ def backup_dashboards(
logger: SupersetLogger,
config: BackupConfig
) -> bool:
- logger.info(f"[STATE][backup_dashboards][ENTER] Starting backup for {env_name}.")
+ logger.info(f"[backup_dashboards][Entry] Starting backup for {env_name}.")
try:
dashboard_count, dashboard_meta = client.get_dashboards()
- logger.info(f"[STATE][backup_dashboards][PROGRESS] Found {dashboard_count} dashboards to export in {env_name}.")
+ logger.info(f"[backup_dashboards][Progress] Found {dashboard_count} dashboards to export in {env_name}.")
if dashboard_count == 0:
return True
@@ -91,8 +96,7 @@ def backup_dashboards(
success_count += 1
except (SupersetAPIError, RequestException, IOError, OSError) as db_error:
- logger.error(f"[STATE][backup_dashboards][FAILURE] Failed to export dashboard {dashboard_title} (ID: {dashboard_id}): {db_error}", exc_info=True)
- # Продолжаем обработку других дашбордов
+ logger.error(f"[backup_dashboards][Failure] Failed to export dashboard {dashboard_title} (ID: {dashboard_id}): {db_error}", exc_info=True)
continue
if config.consolidate:
@@ -101,21 +105,22 @@ def backup_dashboards(
if config.clean_folders:
remove_empty_directories(str(backup_root / env_name), logger=logger)
+ logger.info(f"[backup_dashboards][CoherenceCheck:Passed] Backup logic completed.")
return success_count == dashboard_count
except (RequestException, IOError) as e:
- logger.critical(f"[STATE][backup_dashboards][FAILURE] Fatal error during backup for {env_name}: {e}", exc_info=True)
+ logger.critical(f"[backup_dashboards][Failure] Fatal error during backup for {env_name}: {e}", exc_info=True)
return False
-# END_FUNCTION_backup_dashboards
+#
-# [ENTITY: Function('main')]
-# CONTRACT:
-# PURPOSE: Основная точка входа скрипта.
-# PRECONDITIONS: None
-# POSTCONDITIONS: Возвращает код выхода.
+#
+# @PURPOSE: Основная точка входа для запуска процесса резервного копирования.
+# @RETURN: int - Код выхода (0 - успех, 1 - ошибка).
+# @RELATION: CALLS -> setup_clients
+# @RELATION: CALLS -> backup_dashboards
def main() -> int:
log_dir = Path("P:\\Superset\\010 Бекапы\\Logs")
logger = SupersetLogger(log_dir=log_dir, level=logging.INFO, console=True)
- logger.info("[STATE][main][ENTER] Starting Superset backup process.")
+ logger.info("[main][Entry] Starting Superset backup process.")
exit_code = 0
try:
@@ -137,20 +142,23 @@ def main() -> int:
config=backup_config
)
except Exception as env_error:
- logger.critical(f"[STATE][main][FAILURE] Critical error for environment {env}: {env_error}", exc_info=True)
- # Продолжаем обработку других окружений
+ logger.critical(f"[main][Failure] Critical error for environment {env}: {env_error}", exc_info=True)
results[env] = False
if not all(results.values()):
exit_code = 1
except (RequestException, IOError) as e:
- logger.critical(f"[STATE][main][FAILURE] Fatal error in main execution: {e}", exc_info=True)
+ logger.critical(f"[main][Failure] Fatal error in main execution: {e}", exc_info=True)
exit_code = 1
- logger.info("[STATE][main][SUCCESS] Superset backup process finished.")
+ logger.info("[main][Exit] Superset backup process finished.")
return exit_code
-# END_FUNCTION_main
+#
if __name__ == "__main__":
sys.exit(main())
+
+# --- Конец кода модуля ---
+
+#
diff --git a/comment_mapping.xlsx b/comment_mapping.xlsx
new file mode 100644
index 0000000..ea20157
Binary files /dev/null and b/comment_mapping.xlsx differ
diff --git a/get_dataset_structure.py b/get_dataset_structure.py
new file mode 100644
index 0000000..4c17045
--- /dev/null
+++ b/get_dataset_structure.py
@@ -0,0 +1,69 @@
+#
+# @SEMANTICS: superset, dataset, structure, debug, json
+# @PURPOSE: Этот модуль предназначен для получения и сохранения структуры данных датасета из Superset. Он используется для отладки и анализа данных, возвращаемых API.
+# @DEPENDS_ON: superset_tool.client -> Использует SupersetClient для взаимодействия с API.
+# @DEPENDS_ON: superset_tool.utils.init_clients -> Для инициализации клиентов Superset.
+# @DEPENDS_ON: superset_tool.utils.logger -> Для логирования.
+
+#
+import argparse
+import json
+from superset_tool.utils.init_clients import setup_clients
+from superset_tool.utils.logger import SupersetLogger
+#
+
+# --- Начало кода модуля ---
+
+#
+# @PURPOSE: Получает структуру датасета из Superset и сохраняет ее в JSON-файл.
+# @PARAM: env: str - Среда (dev, prod, и т.д.) для подключения.
+# @PARAM: dataset_id: int - ID датасета для получения.
+# @PARAM: output_path: str - Путь для сохранения JSON-файла.
+# @RELATION: CALLS -> setup_clients
+# @RELATION: CALLS -> superset_client.get_dataset
+def get_and_save_dataset(env: str, dataset_id: int, output_path: str):
+ """
+ Получает структуру датасета и сохраняет в файл.
+ """
+ logger = SupersetLogger(name="DatasetStructureRetriever")
+ logger.info("[get_and_save_dataset][Enter] Starting to fetch dataset structure for ID %d from env '%s'.", dataset_id, env)
+
+ try:
+ clients = setup_clients(logger=logger)
+ superset_client = clients.get(env)
+ if not superset_client:
+ logger.error("[get_and_save_dataset][Failure] Environment '%s' not found.", env)
+ return
+
+ dataset_response = superset_client.get_dataset(dataset_id)
+ dataset_data = dataset_response.get('result')
+
+ if not dataset_data:
+ logger.error("[get_and_save_dataset][Failure] No result in dataset response.")
+ return
+
+ with open(output_path, 'w', encoding='utf-8') as f:
+ json.dump(dataset_data, f, ensure_ascii=False, indent=4)
+
+ logger.info("[get_and_save_dataset][Success] Dataset structure saved to %s.", output_path)
+
+ except Exception as e:
+ logger.error("[get_and_save_dataset][Failure] An error occurred: %s", e, exc_info=True)
+
+#
+
+#
+# @PURPOSE: Точка входа для CLI. Парсит аргументы и запускает получение структуры датасета.
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Получение структуры датасета из Superset.")
+ parser.add_argument("--dataset-id", required=True, type=int, help="ID датасета.")
+ parser.add_argument("--env", required=True, help="Среда для подключения (например, dev).")
+ parser.add_argument("--output-path", default="dataset_structure.json", help="Путь для сохранения JSON-файла.")
+ args = parser.parse_args()
+
+ get_and_save_dataset(args.env, args.dataset_id, args.output_path)
+#
+
+# --- Конец кода модуля ---
+
+#
\ No newline at end of file
diff --git a/migration_script.py b/migration_script.py
index 62059ff..6d1b450 100644
--- a/migration_script.py
+++ b/migration_script.py
@@ -1,72 +1,37 @@
-# [MODULE_PATH] superset_tool.migration_script
-# [FILE] migration_script.py
-# [SEMANTICS] migration, cli, superset, ui, logging, fallback, error-recovery, non-interactive, temp-files, batch-delete
+#
+# @SEMANTICS: migration, cli, superset, ui, logging, error-recovery, batch-delete
+# @PURPOSE: Предоставляет интерактивный CLI для миграции дашбордов Superset между окружениями с возможностью восстановления после ошибок.
+# @DEPENDS_ON: superset_tool.client -> Для взаимодействия с API Superset.
+# @DEPENDS_ON: superset_tool.utils -> Для инициализации клиентов, работы с файлами, UI и логирования.
-# --------------------------------------------------------------
-# [IMPORTS]
-# --------------------------------------------------------------
+#
import json
import logging
import sys
import zipfile
from pathlib import Path
from typing import List, Optional, Tuple, Dict
-
from superset_tool.client import SupersetClient
from superset_tool.utils.init_clients import setup_clients
-from superset_tool.utils.fileio import (
- create_temp_file, # новый контекстный менеджер
- update_yamls,
- create_dashboard_export,
-)
-from superset_tool.utils.whiptail_fallback import (
- menu,
- checklist,
- yesno,
- msgbox,
- inputbox,
- gauge,
-)
+from superset_tool.utils.fileio import create_temp_file, update_yamls, create_dashboard_export
+from superset_tool.utils.whiptail_fallback import menu, checklist, yesno, msgbox, inputbox, gauge
+from superset_tool.utils.logger import SupersetLogger
+#
-from superset_tool.utils.logger import SupersetLogger # type: ignore
-# [END_IMPORTS]
-
-# --------------------------------------------------------------
-# [ENTITY: Service('Migration')]
-# [RELATION: Service('Migration')] -> [DEPENDS_ON] -> [PythonModule('superset_tool.client')]
-# --------------------------------------------------------------
-"""
-:purpose: Интерактивный процесс миграции дашбордов с возможностью
- «удалить‑и‑перезаписать» при ошибке импорта.
-:preconditions:
- - Конфигурация Superset‑клиентов доступна,
- - Пользователь может взаимодействовать через консольный UI.
-:postconditions:
- - Выбранные дашборды импортированы в целевое окружение.
-:sideeffect: Записывает журнал в каталог ``logs/`` текущего рабочего каталога.
-"""
+# --- Начало кода модуля ---
+#
+# @PURPOSE: Инкапсулирует логику интерактивной миграции дашбордов с возможностью «удалить‑и‑перезаписать» при ошибке импорта.
+# @RELATION: CREATES_INSTANCE_OF -> SupersetLogger
+# @RELATION: USES -> SupersetClient
class Migration:
"""
- :ivar SupersetLogger logger: Логгер.
- :ivar bool enable_delete_on_failure: Флаг «удалять‑при‑ошибке».
- :ivar SupersetClient from_c: Клиент‑источник.
- :ivar SupersetClient to_c: Клиент‑назначение.
- :ivar List[dict] dashboards_to_migrate: Список выбранных дашбордов.
- :ivar Optional[dict] db_config_replacement: Параметры замены имён БД.
- :ivar List[dict] _failed_imports: Внутренний буфер неудавшихся импортов
- (ключи: slug, zip_content, dash_id).
- """
-
- # --------------------------------------------------------------
- # [ENTITY: Method('__init__')]
- # --------------------------------------------------------------
- """
- :purpose: Создать сервис миграции и настроить логгер.
- :preconditions: None.
- :postconditions: ``self.logger`` готов к использованию; ``enable_delete_on_failure`` = ``False``.
+ Интерактивный процесс миграции дашбордов.
"""
def __init__(self) -> None:
+ #
+ # @PURPOSE: Инициализирует сервис миграции, настраивает логгер и начальные состояния.
+ # @POST: `self.logger` готов к использованию; `enable_delete_on_failure` = `False`.
default_log_dir = Path.cwd() / "logs"
self.logger = SupersetLogger(
name="migration_script",
@@ -79,62 +44,57 @@ class Migration:
self.to_c: Optional[SupersetClient] = None
self.dashboards_to_migrate: List[dict] = []
self.db_config_replacement: Optional[dict] = None
- self._failed_imports: List[dict] = [] # <-- буфер ошибок
+ self._failed_imports: List[dict] = []
assert self.logger is not None, "Logger must be instantiated."
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('run')]
- # --------------------------------------------------------------
- """
- :purpose: Точка входа – последовательный запуск всех шагов миграции.
- :preconditions: Логгер готов.
- :postconditions: Скрипт завершён, пользователю выведено сообщение.
- """
+ #
+ # @PURPOSE: Точка входа – последовательный запуск всех шагов миграции.
+ # @PRE: Логгер готов.
+ # @POST: Скрипт завершён, пользователю выведено сообщение.
+ # @RELATION: CALLS -> self.ask_delete_on_failure
+ # @RELATION: CALLS -> self.select_environments
+ # @RELATION: CALLS -> self.select_dashboards
+ # @RELATION: CALLS -> self.confirm_db_config_replacement
+ # @RELATION: CALLS -> self.execute_migration
def run(self) -> None:
- self.logger.info("[INFO][run][ENTER] Запуск скрипта миграции.")
+ self.logger.info("[run][Entry] Запуск скрипта миграции.")
self.ask_delete_on_failure()
self.select_environments()
self.select_dashboards()
self.confirm_db_config_replacement()
self.execute_migration()
- self.logger.info("[INFO][run][EXIT] Скрипт миграции завершён.")
- # [END_ENTITY]
+ self.logger.info("[run][Exit] Скрипт миграции завершён.")
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('ask_delete_on_failure')]
- # --------------------------------------------------------------
- """
- :purpose: Запросить у пользователя, следует ли удалять дашборд при ошибке импорта.
- :preconditions: None.
- :postconditions: ``self.enable_delete_on_failure`` установлен.
- """
+ #
+ # @PURPOSE: Запрашивает у пользователя, следует ли удалять дашборд при ошибке импорта.
+ # @POST: `self.enable_delete_on_failure` установлен.
+ # @RELATION: CALLS -> yesno
def ask_delete_on_failure(self) -> None:
self.enable_delete_on_failure = yesno(
"Поведение при ошибке импорта",
"Если импорт завершится ошибкой, удалить существующий дашборд и попытаться импортировать заново?",
)
self.logger.info(
- "[INFO][ask_delete_on_failure] Delete‑on‑failure = %s",
+ "[ask_delete_on_failure][State] Delete-on-failure = %s",
self.enable_delete_on_failure,
)
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('select_environments')]
- # --------------------------------------------------------------
- """
- :purpose: Выбрать исходное и целевое окружения Superset.
- :preconditions: ``setup_clients`` успешно инициализирует все клиенты.
- :postconditions: ``self.from_c`` и ``self.to_c`` установлены.
- """
+ #
+ # @PURPOSE: Позволяет пользователю выбрать исходное и целевое окружения Superset.
+ # @PRE: `setup_clients` успешно инициализирует все клиенты.
+ # @POST: `self.from_c` и `self.to_c` установлены.
+ # @RELATION: CALLS -> setup_clients
+ # @RELATION: CALLS -> menu
def select_environments(self) -> None:
- self.logger.info("[INFO][select_environments][ENTER] Шаг 1/5: Выбор окружений.")
+ self.logger.info("[select_environments][Entry] Шаг 1/5: Выбор окружений.")
try:
all_clients = setup_clients(self.logger)
available_envs = list(all_clients.keys())
except Exception as e:
- self.logger.error("[ERROR][select_environments] %s", e, exc_info=True)
+ self.logger.error("[select_environments][Failure] %s", e, exc_info=True)
msgbox("Ошибка", "Не удалось инициализировать клиенты.")
return
@@ -146,7 +106,7 @@ class Migration:
if rc != 0:
return
self.from_c = all_clients[from_env_name]
- self.logger.info("[INFO][select_environments] from = %s", from_env_name)
+ self.logger.info("[select_environments][State] from = %s", from_env_name)
available_envs.remove(from_env_name)
rc, to_env_name = menu(
@@ -157,24 +117,22 @@ class Migration:
if rc != 0:
return
self.to_c = all_clients[to_env_name]
- self.logger.info("[INFO][select_environments] to = %s", to_env_name)
- self.logger.info("[INFO][select_environments][EXIT] Шаг 1 завершён.")
- # [END_ENTITY]
+ self.logger.info("[select_environments][State] to = %s", to_env_name)
+ self.logger.info("[select_environments][Exit] Шаг 1 завершён.")
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('select_dashboards')]
- # --------------------------------------------------------------
- """
- :purpose: Позволить пользователю выбрать набор дашбордов для миграции.
- :preconditions: ``self.from_c`` инициализирован.
- :postconditions: ``self.dashboards_to_migrate`` заполнен.
- """
+ #
+ # @PURPOSE: Позволяет пользователю выбрать набор дашбордов для миграции.
+ # @PRE: `self.from_c` инициализирован.
+ # @POST: `self.dashboards_to_migrate` заполнен.
+ # @RELATION: CALLS -> self.from_c.get_dashboards
+ # @RELATION: CALLS -> checklist
def select_dashboards(self) -> None:
- self.logger.info("[INFO][select_dashboards][ENTER] Шаг 2/5: Выбор дашбордов.")
+ self.logger.info("[select_dashboards][Entry] Шаг 2/5: Выбор дашбордов.")
try:
- _, all_dashboards = self.from_c.get_dashboards() # type: ignore[attr-defined]
+ _, all_dashboards = self.from_c.get_dashboards()
if not all_dashboards:
- self.logger.warning("[WARN][select_dashboards] No dashboards.")
+ self.logger.warning("[select_dashboards][State] No dashboards.")
msgbox("Информация", "В исходном окружении нет дашбордов.")
return
@@ -192,251 +150,129 @@ class Migration:
if "ALL" in selected:
self.dashboards_to_migrate = list(all_dashboards)
- self.logger.info(
- "[INFO][select_dashboards] Выбраны все дашборды (%d).",
- len(self.dashboards_to_migrate),
- )
- return
-
- self.dashboards_to_migrate = [
- d for d in all_dashboards if str(d["id"]) in selected
- ]
+ else:
+ self.dashboards_to_migrate = [
+ d for d in all_dashboards if str(d["id"]) in selected
+ ]
+
self.logger.info(
- "[INFO][select_dashboards] Выбрано %d дашбордов.",
+ "[select_dashboards][State] Выбрано %d дашбордов.",
len(self.dashboards_to_migrate),
)
except Exception as e:
- self.logger.error("[ERROR][select_dashboards] %s", e, exc_info=True)
+ self.logger.error("[select_dashboards][Failure] %s", e, exc_info=True)
msgbox("Ошибка", "Не удалось получить список дашбордов.")
- self.logger.info("[INFO][select_dashboards][EXIT] Шаг 2 завершён.")
- # [END_ENTITY]
+ self.logger.info("[select_dashboards][Exit] Шаг 2 завершён.")
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('confirm_db_config_replacement')]
- # --------------------------------------------------------------
- """
- :purpose: Запросить у пользователя, требуется ли заменить имена БД в YAML‑файлах.
- :preconditions: None.
- :postconditions: ``self.db_config_replacement`` либо ``None``, либо заполнен.
- """
+ #
+ # @PURPOSE: Запрашивает у пользователя, требуется ли заменить имена БД в YAML-файлах.
+ # @POST: `self.db_config_replacement` либо `None`, либо заполнен.
+ # @RELATION: CALLS -> yesno
+ # @RELATION: CALLS -> inputbox
def confirm_db_config_replacement(self) -> None:
if yesno("Замена БД", "Заменить конфигурацию БД в YAML‑файлах?"):
rc, old_name = inputbox("Замена БД", "Старое имя БД (например, db_dev):")
- if rc != 0:
- return
+ if rc != 0: return
rc, new_name = inputbox("Замена БД", "Новое имя БД (например, db_prod):")
- if rc != 0:
- return
- self.db_config_replacement = {
- "old": {"database_name": old_name},
- "new": {"database_name": new_name},
- }
- self.logger.info(
- "[INFO][confirm_db_config_replacement] Replacement set: %s",
- self.db_config_replacement,
- )
+ if rc != 0: return
+
+ self.db_config_replacement = { "old": {"database_name": old_name}, "new": {"database_name": new_name} }
+ self.logger.info("[confirm_db_config_replacement][State] Replacement set: %s", self.db_config_replacement)
else:
- self.logger.info("[INFO][confirm_db_config_replacement] Skipped.")
- # [END_ENTITY]
+ self.logger.info("[confirm_db_config_replacement][State] Skipped.")
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_batch_delete_by_ids')]
- # --------------------------------------------------------------
- """
- :purpose: Удалить набор дашбордов по их ID единым запросом.
- :preconditions:
- - ``ids`` – непустой список целых чисел.
- :postconditions: Все указанные дашборды удалены (если они существовали).
- :sideeffect: Делает HTTP‑запрос ``DELETE /dashboard/?q=[ids]``.
- """
+ #
+ # @PURPOSE: Удаляет набор дашбордов по их ID единым запросом.
+ # @PRE: `ids` – непустой список целых чисел.
+ # @POST: Все указанные дашборды удалены (если они существовали).
+ # @PARAM: ids: List[int] - Список ID дашбордов для удаления.
+ # @RELATION: CALLS -> self.to_c.network.request
def _batch_delete_by_ids(self, ids: List[int]) -> None:
if not ids:
- self.logger.debug("[DEBUG][_batch_delete_by_ids] Empty ID list – nothing to delete.")
+ self.logger.debug("[_batch_delete_by_ids][Skip] Empty ID list – nothing to delete.")
return
- self.logger.info("[INFO][_batch_delete_by_ids] Deleting dashboards IDs: %s", ids)
- # Формируем параметр q в виде JSON‑массива, как требует Superset.
+ self.logger.info("[_batch_delete_by_ids][Entry] Deleting dashboards IDs: %s", ids)
q_param = json.dumps(ids)
- response = self.to_c.network.request(
- method="DELETE",
- endpoint="/dashboard/",
- params={"q": q_param},
- )
- # Superset обычно отвечает 200/204; проверяем поле ``result`` при наличии.
+ response = self.to_c.network.request(method="DELETE", endpoint="/dashboard/", params={"q": q_param})
+
if isinstance(response, dict) and response.get("result", True) is False:
- self.logger.warning("[WARN][_batch_delete_by_ids] Unexpected delete response: %s", response)
+ self.logger.warning("[_batch_delete_by_ids][Warning] Unexpected delete response: %s", response)
else:
- self.logger.info("[INFO][_batch_delete_by_ids] Delete request completed.")
- # [END_ENTITY]
+ self.logger.info("[_batch_delete_by_ids][Success] Delete request completed.")
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('execute_migration')]
- # --------------------------------------------------------------
- """
- :purpose: Выполнить экспорт‑импорт выбранных дашбордов, при необходимости
- обновив YAML‑файлы. При ошибке импортов сохраняем slug, а потом
- удаляем проблемные дашборды **по ID**, получив их через slug.
- :preconditions:
- - ``self.dashboards_to_migrate`` не пуст,
- - ``self.from_c`` и ``self.to_c`` инициализированы.
- :postconditions:
- - Все успешные дашборды импортированы,
- - Неудачные дашборды, если пользователь выбрал «удалять‑при‑ошибке»,
- удалены и повторно импортированы.
- :sideeffect: При включённом флаге ``enable_delete_on_failure`` производится
- батч‑удаление и повторный импорт.
- """
+ #
+ # @PURPOSE: Выполняет экспорт-импорт дашбордов, обрабатывает ошибки и, при необходимости, выполняет процедуру восстановления.
+ # @PRE: `self.dashboards_to_migrate` не пуст; `self.from_c` и `self.to_c` инициализированы.
+ # @POST: Успешные дашборды импортированы; неудачные - восстановлены или залогированы.
+ # @RELATION: CALLS -> self.from_c.export_dashboard
+ # @RELATION: CALLS -> create_temp_file
+ # @RELATION: CALLS -> update_yamls
+ # @RELATION: CALLS -> create_dashboard_export
+ # @RELATION: CALLS -> self.to_c.import_dashboard
+ # @RELATION: CALLS -> self._batch_delete_by_ids
def execute_migration(self) -> None:
if not self.dashboards_to_migrate:
- self.logger.warning("[WARN][execute_migration] No dashboards to migrate.")
+ self.logger.warning("[execute_migration][Skip] No dashboards to migrate.")
msgbox("Информация", "Нет дашбордов для миграции.")
return
total = len(self.dashboards_to_migrate)
- self.logger.info("[INFO][execute_migration] Starting migration of %d dashboards.", total)
+ self.logger.info("[execute_migration][Entry] Starting migration of %d dashboards.", total)
+ self.to_c.delete_before_reimport = self.enable_delete_on_failure
- # Передаём режим клиенту‑назначению
- self.to_c.delete_before_reimport = self.enable_delete_on_failure # type: ignore[attr-defined]
-
- # -----------------------------------------------------------------
- # 1️⃣ Основной проход – экспорт → импорт → сбор ошибок
- # -----------------------------------------------------------------
with gauge("Миграция...", width=60, height=10) as g:
for i, dash in enumerate(self.dashboards_to_migrate):
- dash_id = dash["id"]
- dash_slug = dash.get("slug") # slug нужен для дальнейшего поиска
- title = dash["dashboard_title"]
-
- progress = int((i / total) * 100)
+ dash_id, dash_slug, title = dash["id"], dash.get("slug"), dash["dashboard_title"]
g.set_text(f"Миграция: {title} ({i + 1}/{total})")
- g.set_percent(progress)
+ g.set_percent(int((i / total) * 100))
try:
- # ------------------- Экспорт -------------------
- exported_content, _ = self.from_c.export_dashboard(dash_id) # type: ignore[attr-defined]
-
- # ------------------- Временный ZIP -------------------
- with create_temp_file(
- content=exported_content,
- suffix=".zip",
- logger=self.logger,
- ) as tmp_zip_path:
- self.logger.debug("[DEBUG][temp_zip] Temporary ZIP at %s", tmp_zip_path)
-
- # ------------------- Распаковка во временный каталог -------------------
- with create_temp_file(suffix=".dir", logger=self.logger) as tmp_unpack_dir:
- self.logger.debug("[DEBUG][temp_dir] Temporary unpack dir: %s", tmp_unpack_dir)
-
- with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref:
- zip_ref.extractall(tmp_unpack_dir)
- self.logger.info("[INFO][execute_migration] Export unpacked to %s", tmp_unpack_dir)
-
- # ------------------- YAML‑обновление (если нужно) -------------------
- if self.db_config_replacement:
- update_yamls(
- db_configs=[self.db_config_replacement],
- path=str(tmp_unpack_dir),
- )
- self.logger.info("[INFO][execute_migration] YAML‑files updated.")
-
- # ------------------- Сборка нового ZIP -------------------
- with create_temp_file(suffix=".zip", logger=self.logger) as tmp_new_zip:
- create_dashboard_export(
- zip_path=tmp_new_zip,
- source_paths=[str(tmp_unpack_dir)],
- )
- self.logger.info("[INFO][execute_migration] Re‑packed to %s", tmp_new_zip)
-
- # ------------------- Импорт -------------------
- self.to_c.import_dashboard(
- file_name=tmp_new_zip,
- dash_id=dash_id,
- dash_slug=dash_slug,
- ) # type: ignore[attr-defined]
-
- # Если импорт прошёл без исключений – фиксируем успех
- self.logger.info("[INFO][execute_migration][SUCCESS] Dashboard %s imported.", title)
-
+ exported_content, _ = self.from_c.export_dashboard(dash_id)
+ with create_temp_file(content=exported_content, suffix=".zip", logger=self.logger) as tmp_zip_path, \
+ create_temp_file(suffix=".dir", logger=self.logger) as tmp_unpack_dir:
+
+ with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref:
+ zip_ref.extractall(tmp_unpack_dir)
+
+ if self.db_config_replacement:
+ update_yamls(db_configs=[self.db_config_replacement], path=str(tmp_unpack_dir))
+
+ with create_temp_file(suffix=".zip", logger=self.logger) as tmp_new_zip:
+ create_dashboard_export(zip_path=tmp_new_zip, source_paths=[str(tmp_unpack_dir)])
+ self.to_c.import_dashboard(file_name=tmp_new_zip, dash_id=dash_id, dash_slug=dash_slug)
+
+ self.logger.info("[execute_migration][Success] Dashboard %s imported.", title)
except Exception as exc:
- # Сохраняем данные для повторного импорта после batch‑удаления
- self.logger.error("[ERROR][execute_migration] %s", exc, exc_info=True)
- self._failed_imports.append(
- {
- "slug": dash_slug,
- "dash_id": dash_id,
- "zip_content": exported_content,
- }
- )
+ self.logger.error("[execute_migration][Failure] %s", exc, exc_info=True)
+ self._failed_imports.append({"slug": dash_slug, "dash_id": dash_id, "zip_content": exported_content})
msgbox("Ошибка", f"Не удалось мигрировать дашборд {title}.\n\n{exc}")
+ g.set_percent(100)
- g.set_percent(100)
-
- # -----------------------------------------------------------------
- # 2️⃣ Если возникли ошибки и пользователь согласился удалять – удаляем и повторяем
- # -----------------------------------------------------------------
if self.enable_delete_on_failure and self._failed_imports:
- self.logger.info(
- "[INFO][execute_migration] %d dashboards failed. Starting recovery procedure.",
- len(self._failed_imports),
- )
-
- # ------------------- Получаем список дашбордов в целевом окружении -------------------
- _, target_dashboards = self.to_c.get_dashboards() # type: ignore[attr-defined]
- slug_to_id: Dict[str, int] = {
- d["slug"]: d["id"] for d in target_dashboards if "slug" in d and "id" in d
- }
-
- # ------------------- Формируем список ID‑ов для удаления -------------------
- ids_to_delete: List[int] = []
- for fail in self._failed_imports:
- slug = fail["slug"]
- if slug and slug in slug_to_id:
- ids_to_delete.append(slug_to_id[slug])
- else:
- self.logger.warning(
- "[WARN][execute_migration] Unable to map slug '%s' to ID on target.",
- slug,
- )
-
- # ------------------- Batch‑удаление -------------------
+ self.logger.info("[execute_migration][Recovery] %d dashboards failed. Starting recovery.", len(self._failed_imports))
+ _, target_dashboards = self.to_c.get_dashboards()
+ slug_to_id = {d["slug"]: d["id"] for d in target_dashboards if "slug" in d and "id" in d}
+ ids_to_delete = [slug_to_id[f["slug"]] for f in self._failed_imports if f["slug"] in slug_to_id]
self._batch_delete_by_ids(ids_to_delete)
- # ------------------- Повторный импорт только для проблемных дашбордов -------------------
for fail in self._failed_imports:
- dash_slug = fail["slug"]
- dash_id = fail["dash_id"]
- zip_content = fail["zip_content"]
+ with create_temp_file(content=fail["zip_content"], suffix=".zip", logger=self.logger) as retry_zip:
+ self.to_c.import_dashboard(file_name=retry_zip, dash_id=fail["dash_id"], dash_slug=fail["slug"])
+ self.logger.info("[execute_migration][Recovered] Dashboard slug '%s' re-imported.", fail["slug"])
- # Один раз создаём временный ZIP‑файл из сохранённого содержимого
- with create_temp_file(
- content=zip_content,
- suffix=".zip",
- logger=self.logger,
- ) as retry_zip_path:
- self.logger.debug("[DEBUG][retry_zip] Retry ZIP for slug %s at %s", dash_slug, retry_zip_path)
-
- # Пере‑импортируем – **slug** передаётся, но клиент будет использовать ID
- self.to_c.import_dashboard(
- file_name=retry_zip_path,
- dash_id=dash_id,
- dash_slug=dash_slug,
- ) # type: ignore[attr-defined]
-
- self.logger.info("[INFO][execute_migration][RECOVERED] Dashboard slug '%s' re‑imported.", dash_slug)
-
- # -----------------------------------------------------------------
- # 3️⃣ Финальная отчётность
- # -----------------------------------------------------------------
- self.logger.info("[INFO][execute_migration] Migration finished.")
+ self.logger.info("[execute_migration][Exit] Migration finished.")
msgbox("Информация", "Миграция завершена!")
- # [END_ENTITY]
+ #
-# [END_ENTITY: Service('Migration')]
+#
+
+# --- Конец кода модуля ---
-# --------------------------------------------------------------
-# Точка входа
-# --------------------------------------------------------------
if __name__ == "__main__":
Migration().run()
-# [END_FILE migration_script.py]
-# --------------------------------------------------------------
\ No newline at end of file
+
+#
\ No newline at end of file
diff --git a/run_mapper.py b/run_mapper.py
new file mode 100644
index 0000000..99d405a
--- /dev/null
+++ b/run_mapper.py
@@ -0,0 +1,72 @@
+#
+# @SEMANTICS: runner, configuration, cli, main
+# @PURPOSE: Этот модуль является CLI-точкой входа для запуска процесса меппинга метаданных датасетов.
+# @DEPENDS_ON: dataset_mapper -> Использует DatasetMapper для выполнения основной логики.
+# @DEPENDS_ON: superset_tool.utils -> Для инициализации клиентов и логирования.
+
+#
+import argparse
+from superset_tool.utils.init_clients import setup_clients
+from superset_tool.utils.logger import SupersetLogger
+from superset_tool.utils.dataset_mapper import DatasetMapper
+#
+
+# --- Начало кода модуля ---
+
+#
+# @PURPOSE: Парсит аргументы командной строки и запускает процесс меппинга.
+# @RELATION: CREATES_INSTANCE_OF -> DatasetMapper
+# @RELATION: CALLS -> setup_clients
+# @RELATION: CALLS -> DatasetMapper.run_mapping
+def main():
+ parser = argparse.ArgumentParser(description="Map dataset verbose names in Superset.")
+ parser.add_argument('--source', type=str, required=True, choices=['postgres', 'excel', 'both'], help='The source for the mapping.')
+ parser.add_argument('--dataset-id', type=int, required=True, help='The ID of the dataset to update.')
+ parser.add_argument('--table-name', type=str, help='The table name for PostgreSQL source.')
+ parser.add_argument('--table-schema', type=str, help='The table schema for PostgreSQL source.')
+ parser.add_argument('--excel-path', type=str, help='The path to the Excel file.')
+ parser.add_argument('--env', type=str, default='dev', help='The Superset environment to use.')
+
+ args = parser.parse_args()
+ logger = SupersetLogger(name="dataset_mapper_main")
+
+ # [AI_NOTE]: Конфигурация БД должна быть вынесена во внешний файл или переменные окружения.
+ POSTGRES_CONFIG = {
+ 'dbname': 'dwh',
+ 'user': 'your_user',
+ 'password': 'your_password',
+ 'host': 'your_host',
+ 'port': 'your_port'
+ }
+
+ logger.info("[main][Enter] Starting dataset mapper CLI.")
+ try:
+ clients = setup_clients(logger)
+ superset_client = clients.get(args.env)
+
+ if not superset_client:
+ logger.error(f"[main][Failure] Superset client for '{args.env}' environment not found.")
+ return
+
+ mapper = DatasetMapper(logger)
+ mapper.run_mapping(
+ superset_client=superset_client,
+ dataset_id=args.dataset_id,
+ source=args.source,
+ postgres_config=POSTGRES_CONFIG if args.source in ['postgres', 'both'] else None,
+ excel_path=args.excel_path if args.source in ['excel', 'both'] else None,
+ table_name=args.table_name if args.source in ['postgres', 'both'] else None,
+ table_schema=args.table_schema if args.source in ['postgres', 'both'] else None
+ )
+ logger.info("[main][Exit] Dataset mapper process finished.")
+
+ except Exception as main_exc:
+ logger.error("[main][Failure] An unexpected error occurred: %s", main_exc, exc_info=True)
+#
+
+if __name__ == '__main__':
+ main()
+
+# --- Конец кода модуля ---
+
+#
\ No newline at end of file
diff --git a/search_script.py b/search_script.py
index bd864b4..d34c83b 100644
--- a/search_script.py
+++ b/search_script.py
@@ -1,88 +1,88 @@
-# 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.
-"""
+#
+# @SEMANTICS: search, superset, dataset, regex
+# @PURPOSE: Предоставляет утилиты для поиска по текстовым паттернам в метаданных датасетов Superset.
+# @DEPENDS_ON: superset_tool.client -> Для взаимодействия с API Superset.
+# @DEPENDS_ON: superset_tool.utils -> Для логирования и инициализации клиентов.
-# [IMPORTS] Стандартная библиотека
+#
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.exceptions import SupersetAPIError
from superset_tool.utils.logger import SupersetLogger
from superset_tool.utils.init_clients import setup_clients
+#
-# [ENTITY: Function('search_datasets')]
-# CONTRACT:
-# PURPOSE: Выполняет поиск по строковому паттерну в метаданных всех датасетов.
-# PRECONDITIONS:
-# - `client` должен быть инициализированным экземпляром `SupersetClient`.
-# - `search_pattern` должен быть валидной строкой регулярного выражения.
-# POSTCONDITIONS:
-# - Возвращает словарь с результатами поиска.
+# --- Начало кода модуля ---
+
+#
+# @PURPOSE: Выполняет поиск по строковому паттерну в метаданных всех датасетов.
+# @PRE: `client` должен быть инициализированным экземпляром `SupersetClient`.
+# @PRE: `search_pattern` должен быть валидной строкой регулярного выражения.
+# @POST: Возвращает словарь с результатами поиска, где ключ - ID датасета, значение - список совпадений.
+# @PARAM: client: SupersetClient - Клиент для доступа к API Superset.
+# @PARAM: search_pattern: str - Регулярное выражение для поиска.
+# @PARAM: logger: Optional[SupersetLogger] - Инстанс логгера.
+# @RETURN: Optional[Dict] - Словарь с результатами или None, если ничего не найдено.
+# @THROW: re.error - Если паттерн регулярного выражения невалиден.
+# @THROW: SupersetAPIError, RequestException - При критических ошибках API.
+# @RELATION: CALLS -> client.get_datasets
def search_datasets(
client: SupersetClient,
search_pattern: str,
logger: Optional[SupersetLogger] = None
) -> Optional[Dict]:
logger = logger or SupersetLogger(name="dataset_search")
- logger.info(f"[STATE][search_datasets][ENTER] Searching for pattern: '{search_pattern}'")
+ logger.info(f"[search_datasets][Enter] Searching for pattern: '{search_pattern}'")
try:
- _, datasets = client.get_datasets(query={
- "columns": ["id", "table_name", "sql", "database", "columns"]
- })
+ _, datasets = client.get_datasets(query={"columns": ["id", "table_name", "sql", "database", "columns"]})
if not datasets:
- logger.warning("[STATE][search_datasets][EMPTY] No datasets found.")
+ logger.warning("[search_datasets][State] No datasets found.")
return None
pattern = re.compile(search_pattern, re.IGNORECASE)
results = {}
- available_fields = set(datasets[0].keys())
-
+
for dataset in datasets:
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)
+ for field, value in dataset.items():
+ value_str = str(value)
+ if pattern.search(value_str):
+ match_obj = pattern.search(value_str)
matches.append({
"field": field,
"match": match_obj.group() if match_obj else "",
- "value": value
+ "value": value_str
})
if matches:
results[dataset_id] = matches
- logger.info(f"[STATE][search_datasets][SUCCESS] Found matches in {len(results)} datasets.")
+ logger.info(f"[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)
+ logger.error(f"[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)
+ logger.critical(f"[search_datasets][Failure] Critical error during search: {e}", exc_info=True)
raise
-# END_FUNCTION_search_datasets
+#
-# [ENTITY: Function('print_search_results')]
-# CONTRACT:
-# PURPOSE: Форматирует результаты поиска для читаемого вывода в консоль.
-# PRECONDITIONS:
-# - `results` является словарем, возвращенным `search_datasets`, или `None`.
-# POSTCONDITIONS:
-# - Возвращает отформатированную строку с результатами.
+#
+# @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 "Ничего не найдено"
@@ -91,46 +91,40 @@ def print_search_results(results: Optional[Dict], context_lines: int = 3) -> str
for dataset_id, matches in results.items():
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']
-
+ 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
+ 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_line = max(0, match_line_index - context_lines)
- end_line = min(len(lines), match_line_index + context_lines + 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_line, end_line):
- line_number = i + 1
+ for i in range(start, end):
+ prefix = f"{i + 1:5d}: "
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}")
+ 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)
-# END_FUNCTION_print_search_results
+#
-# [ENTITY: Function('main')]
-# CONTRACT:
-# PURPOSE: Основная точка входа скрипта.
-# PRECONDITIONS: None
-# POSTCONDITIONS: None
+#
+# @PURPOSE: Основная точка входа для запуска скрипта поиска.
+# @RELATION: CALLS -> setup_clients
+# @RELATION: CALLS -> search_datasets
+# @RELATION: CALLS -> print_search_results
def main():
logger = SupersetLogger(level=logging.INFO, console=True)
clients = setup_clients(logger)
@@ -145,8 +139,12 @@ def main():
)
report = print_search_results(results)
- logger.info(f"[STATE][main][SUCCESS] Search finished. Report:\n{report}")
-# END_FUNCTION_main
+ logger.info(f"[main][Success] Search finished. Report:\n{report}")
+#
if __name__ == "__main__":
main()
+
+# --- Конец кода модуля ---
+
+#
diff --git a/semantic_protocol.md b/semantic_protocol.md
new file mode 100644
index 0000000..30f1810
--- /dev/null
+++ b/semantic_protocol.md
@@ -0,0 +1,120 @@
+### **Протокол GRACE-Py: Семантическая Разметка для AI-Агентов на Python**
+
+**Версия: 2.2 (Hybrid)**
+
+#### **I. Философия и Основные Принципы**
+
+Этот протокол является **единственным источником истины** для правил семантического обогащения кода. Его цель — превратить процесс разработки с LLM-агентами из непредсказуемого "диалога" в управляемую **инженерную дисциплину**.
+
+* **Аксиома 1: Код Вторичен.** Первична его семантическая модель (графы, контракты, якоря).
+* **Аксиома 2: Когерентность Абсолютна.** Все артефакты (ТЗ, граф, контракты, код) должны быть на 100% семантически согласованы.
+* **Аксиома 3: Архитектура GPT — Закон.** Протокол построен на фундаментальных принципах работы трансформеров (Causal Attention, KV Cache, Sparse Attention).
+
+#### **II. Структура Файла (`.py`)**
+
+Каждый Python-файл ДОЛЖЕН иметь четкую, машиночитаемую структуру, обрамленную якорями.
+
+```python
+#
+# @SEMANTICS: domain, usecase, data_processing
+# @PURPOSE: Этот модуль отвечает за обработку пользовательских данных.
+# @DEPENDS_ON: utils_module -> Использует утилиты для валидации.
+
+#
+import os
+from typing import List
+#
+
+# --- Начало кода модуля ---
+
+# ... (классы, функции, константы) ...
+
+# --- Конец кода модуля ---
+
+#
+```
+
+#### **III. Компоненты Разметки (Детализация GRACE-Py)**
+
+##### **A. Anchors (Якоря): Навигация и Консолидация**
+
+1. **Назначение:** Якоря — это основной инструмент для управления вниманием ИИ, создания семантических каналов и обеспечения надежной навигации в больших кодовых базах (Sparse Attention).
+2. **Синтаксис:** Используются парные комментарии в псевдо-XML формате.
+ * **Открывающий:** `# `
+ * **Закрывающий (Обязателен!):** `# `
+3. **"Якорь-Аккумулятор":** Закрывающий якорь консолидирует всю семантику блока (контракт + код), создавая мощный вектор для RAG-систем.
+4. **Семантические Каналы:** `id` якоря ДОЛЖЕН совпадать с именем сущности для создания устойчивой семантической связи.
+5. **Таксономия Типов (`type`):** `Module`, `Class`, `Interface`, `Object`, `DataClass`, `SealedInterface`, `EnumClass`, `Function`, `UseCase`, `ViewModel`, `Repository`.
+
+##### **C. Contracts (Контракты): Тактические Спецификации**
+
+1. **Назначение:** Предоставление ИИ точных инструкций для генерации и валидации кода.
+2. **Расположение:** Контракт всегда располагается **внутри открывающего якоря**, ДО декларации кода (`def` или `class`).
+3. **Синтаксис:** JSDoc-подобный стиль с `@tag` для лаконичности и читаемости.
+ ```python
+ #
+ # @PURPOSE: Валидирует и обрабатывает входящие данные пользователя.
+ # @SPEC_LINK: tz-req-005
+ # @PRE: `raw_data` не должен быть пустым.
+ # @POST: Возвращаемый словарь содержит ключ 'is_valid'.
+ # @PARAM: raw_data: Dict[str, any] - Сырые данные от пользователя.
+ # @RETURN: Dict[str, any] - Обработанные и валидированные данные.
+ # @TEST: input='{"user_id": 123}', expected_output='{"is_valid": True}'
+ # @THROW: ValueError - Если 'user_id' отсутствует.
+ # @RELATION: CALLS -> validate_user_id
+ # @CONSTRAINT: Не использовать внешние сетевые вызовы.
+ ```
+4. **Реализация в Коде:** Предусловия и постусловия, описанные в контракте, ДОЛЖНЫ быть реализованы в коде с использованием `assert`, `require()`/`check()` или явных `if...raise`.
+
+##### **G. Graph (Граф Знаний)**
+
+1. **Назначение:** Описание высокоуровневых зависимостей между сущностями.
+2. **Реализация:** Граф определяется тегами `@RELATION` внутри GRACE блока (якоря). Это создает распределенный граф, который легко парсить.
+ * **Синтаксис:** `@: -> [опциональное описание]`
+ * **Таксономия Предикатов (``):** `DEPENDS_ON`, `CALLS`, `CREATES_INSTANCE_OF`, `INHERITS_FROM`, `IMPLEMENTS`, `READS_FROM`, `WRITES_TO`, `DISPATCHES_EVENT`, `OBSERVES`.
+
+##### **E. Evaluation (Логирование)**
+
+1. **Назначение:** Декларация `belief state` агента и обеспечение трассируемости для отладки.
+2. **Формат:** `logger.level(f"[ANCHOR_ID][STATE] Сообщение")`
+ * **`ANCHOR_ID`:** `id` якоря, в котором находится лог.
+ * **`STATE`:** Текущее состояние логики (например, `Entry`, `Validation`, `Exit`, `CoherenceCheckFailed`).
+3. **Пример:** `logger.debug(f"[process_data][Validation] Проверка `raw_data`...")`
+
+#### **IV. Запреты и Ограничения**
+
+1. **Запрет на Обычные Комментарии:** Комментарии в стиле `//` или `/* */` **ЗАПРЕЩЕНЫ**. Вся мета-информация должна быть в структурированных GRACE блоках.
+ * **Исключение:** `# [AI_NOTE]: ...` для прямых указаний агенту в конкретной точке кода.
+
+#### **V. Полный Пример Разметки Функции (GRACE-Py 2.2)**
+
+```python
+#
+# @PURPOSE: Валидирует и обрабатывает входящие данные пользователя.
+# @SPEC_LINK: tz-req-005
+# @PRE: `raw_data` не должен быть пустым.
+# @PARAM: raw_data: Dict[str, any] - Сырые данные от пользователя.
+# @RETURN: Dict[str, any] - Обработанные и валидированные данные.
+# @TEST: input='{}', expected_exception='AssertionError'
+# @RELATION: CALLS -> some_helper_function
+def process_data(raw_data: dict) -> dict:
+ """
+ Docstring для стандартных инструментов Python.
+ Не является источником истины для ИИ-агентов.
+ """
+ logger.debug(f"[process_data][Entry] Начало обработки данных.")
+
+ # Реализация контракта
+ assert raw_data, "Precondition failed: raw_data must not be empty."
+
+ # ... Основная логика ...
+ processed_data = {"is_valid": True}
+ processed_data.update(raw_data)
+
+ logger.info(f"[process_data][CoherenceCheck:Passed] Код соответствует контракту.")
+ logger.debug(f"[process_data][Exit] Завершение обработки.")
+
+ return processed_data
+#
+```
+
diff --git a/superset_tool/client.py b/superset_tool/client.py
index 4002edc..9fefc20 100644
--- a/superset_tool/client.py
+++ b/superset_tool/client.py
@@ -1,59 +1,38 @@
-# [MODULE_PATH] superset_tool.client
-# [FILE] client.py
-# [SEMANTICS] superset, api, client, logging, error-handling, slug-support
+#
+# @SEMANTICS: superset, api, client, rest, http, dashboard, dataset, import, export
+# @PURPOSE: Предоставляет высокоуровневый клиент для взаимодействия с Superset REST API, инкапсулируя логику запросов, обработку ошибок и пагинацию.
+# @DEPENDS_ON: superset_tool.models -> Использует SupersetConfig для конфигурации.
+# @DEPENDS_ON: superset_tool.exceptions -> Выбрасывает специализированные исключения.
+# @DEPENDS_ON: superset_tool.utils -> Использует утилиты для сети, логгирования и работы с файлами.
-# --------------------------------------------------------------
-# [IMPORTS]
-# --------------------------------------------------------------
+#
import json
import zipfile
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
-
from requests import Response
-
from superset_tool.models import SupersetConfig
from superset_tool.exceptions import ExportError, InvalidZipFormatError
from superset_tool.utils.fileio import get_filename_from_headers
from superset_tool.utils.logger import SupersetLogger
from superset_tool.utils.network import APIClient
-# [END_IMPORTS]
+#
-# --------------------------------------------------------------
-# [ENTITY: Service('SupersetClient')]
-# [RELATION: Service('SupersetClient')] -> [DEPENDS_ON] -> [PythonModule('superset_tool.utils.network')]
-# --------------------------------------------------------------
-"""
-:purpose: Класс‑обёртка над Superset REST‑API.
-:preconditions:
- - ``config`` – валидный объект :class:`SupersetConfig`.
- - Доступен рабочий HTTP‑клиент :class:`APIClient`.
-:postconditions:
- - Объект готов к выполнению запросов (GET, POST, DELETE и т.д.).
-:raises:
- - :class:`TypeError` при передаче неверного типа конфигурации.
-"""
+# --- Начало кода модуля ---
+
+#
+# @PURPOSE: Класс-обёртка над Superset REST API, предоставляющий методы для работы с дашбордами и датасетами.
+# @RELATION: CREATES_INSTANCE_OF -> APIClient
+# @RELATION: USES -> SupersetConfig
class SupersetClient:
- """
- :ivar SupersetLogger logger: Логгер, используемый в клиенте.
- :ivar SupersetConfig config: Текущая конфигурация подключения.
- :ivar APIClient network: Объект‑обёртка над ``requests``.
- :ivar bool delete_before_reimport: Флаг, указывающий,
- что при ошибке импорта дашборд следует удалить и повторить импорт.
- """
-
- # --------------------------------------------------------------
- # [ENTITY: Method('__init__')]
- # --------------------------------------------------------------
- """
- :purpose: Инициализировать клиент и передать ему логгер.
- :preconditions: ``config`` – экземпляр :class:`SupersetConfig`.
- :postconditions: Атрибуты ``logger``, ``config`` и ``network`` созданы,
- ``delete_before_reimport`` установлен в ``False``.
- """
def __init__(self, config: SupersetConfig, logger: Optional[SupersetLogger] = None):
+ #
+ # @PURPOSE: Инициализирует клиент, проверяет конфигурацию и создает сетевой клиент.
+ # @PARAM: config: SupersetConfig - Конфигурация подключения.
+ # @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
+ # @POST: Атрибуты `logger`, `config`, и `network` созданы.
self.logger = logger or SupersetLogger(name="SupersetClient")
- self.logger.info("[INFO][SupersetClient.__init__] Initializing SupersetClient.")
+ self.logger.info("[SupersetClient.__init__][Enter] Initializing SupersetClient.")
self._validate_config(config)
self.config = config
self.network = APIClient(
@@ -63,68 +42,52 @@ class SupersetClient:
logger=self.logger,
)
self.delete_before_reimport: bool = False
- self.logger.info("[INFO][SupersetClient.__init__] SupersetClient initialized.")
- # [END_ENTITY]
+ self.logger.info("[SupersetClient.__init__][Exit] SupersetClient initialized.")
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_validate_config')]
- # --------------------------------------------------------------
- """
- :purpose: Проверить, что передан объект :class:`SupersetConfig`.
- :preconditions: ``config`` – произвольный объект.
- :postconditions: При несовпадении типов возбуждается :class:`TypeError`.
- """
+ #
+ # @PURPOSE: Проверяет, что переданный объект конфигурации имеет корректный тип.
+ # @PARAM: config: SupersetConfig - Объект для проверки.
+ # @THROW: TypeError - Если `config` не является экземпляром `SupersetConfig`.
def _validate_config(self, config: SupersetConfig) -> None:
- self.logger.debug("[DEBUG][_validate_config][ENTER] Validating SupersetConfig.")
- if not isinstance(config, SupersetConfig):
- self.logger.error("[ERROR][_validate_config][FAILURE] Invalid config type.")
- raise TypeError("Конфигурация должна быть экземпляром SupersetConfig")
- self.logger.debug("[DEBUG][_validate_config][SUCCESS] Config is valid.")
- # [END_ENTITY]
+ self.logger.debug("[_validate_config][Enter] Validating SupersetConfig.")
+ assert isinstance(config, SupersetConfig), "Конфигурация должна быть экземпляром SupersetConfig"
+ self.logger.debug("[_validate_config][Exit] Config is valid.")
+ #
- # --------------------------------------------------------------
- # [ENTITY: Property('headers')]
- # --------------------------------------------------------------
@property
def headers(self) -> dict:
- """Базовые HTTP‑заголовки, используемые клиентом."""
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('get_dashboards')]
- # --------------------------------------------------------------
- """
- :purpose: Получить список дашбордов с поддержкой пагинации.
- :preconditions: None.
- :postconditions: Возвращается кортеж ``(total_count, list_of_dashboards)``.
- """
+ #
+ # @PURPOSE: Получает полный список дашбордов, автоматически обрабатывая пагинацию.
+ # @PARAM: query: Optional[Dict] - Дополнительные параметры запроса для API.
+ # @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список дашбордов).
+ # @RELATION: CALLS -> self._fetch_total_object_count
+ # @RELATION: CALLS -> self._fetch_all_pages
def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
- self.logger.info("[INFO][get_dashboards][ENTER] Fetching dashboards.")
+ self.logger.info("[get_dashboards][Enter] Fetching dashboards.")
validated_query = self._validate_query_params(query)
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",
- },
+ pagination_options={"base_query": validated_query, "total_count": total_count, "results_field": "result"},
)
- self.logger.info("[INFO][get_dashboards][SUCCESS] Got dashboards.")
+ self.logger.info("[get_dashboards][Exit] Found %d dashboards.", total_count)
return total_count, paginated_data
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('export_dashboard')]
- # --------------------------------------------------------------
- """
- :purpose: Скачать дашборд в виде ZIP‑архива.
- :preconditions: ``dashboard_id`` – существующий идентификатор.
- :postconditions: Возвращается бинарное содержимое и имя файла.
- """
+ #
+ # @PURPOSE: Экспортирует дашборд в виде ZIP-архива.
+ # @PARAM: dashboard_id: int - ID дашборда для экспорта.
+ # @RETURN: Tuple[bytes, str] - Бинарное содержимое ZIP-архива и имя файла.
+ # @THROW: ExportError - Если экспорт завершился неудачей.
+ # @RELATION: CALLS -> self.network.request
def export_dashboard(self, dashboard_id: int) -> Tuple[bytes, str]:
- self.logger.info("[INFO][export_dashboard][ENTER] Exporting dashboard %s.", dashboard_id)
+ self.logger.info("[export_dashboard][Enter] Exporting dashboard %s.", dashboard_id)
response = self.network.request(
method="GET",
endpoint="/dashboard/export/",
@@ -134,160 +97,86 @@ class SupersetClient:
)
self._validate_export_response(response, dashboard_id)
filename = self._resolve_export_filename(response, dashboard_id)
- self.logger.info("[INFO][export_dashboard][SUCCESS] Exported dashboard %s.", dashboard_id)
+ self.logger.info("[export_dashboard][Exit] Exported dashboard %s to %s.", dashboard_id, filename)
return response.content, filename
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('import_dashboard')]
- # --------------------------------------------------------------
- """
- :purpose: Импортировать дашборд из ZIP‑файла. При неуспешном импорте,
- если ``delete_before_reimport`` = True, сначала удаляется
- дашборд по ID, затем импорт повторяется.
- :preconditions:
- - ``file_name`` – путь к существующему ZIP‑архиву (str|Path).
- - ``dash_id`` – (опционально) ID дашборда, который следует удалить.
- :postconditions: Возвращается словарь‑ответ API при успехе.
- """
- def import_dashboard(
- self,
- file_name: Union[str, Path],
- dash_id: Optional[int] = None,
- dash_slug: Optional[str] = None, # сохраняем для возможного логирования
- ) -> Dict:
- # -----------------------------------------------------------------
- # 1️⃣ Приводим путь к строке (API‑клиент ожидает str)
- # -----------------------------------------------------------------
- file_path: str = str(file_name) # <--- гарантируем тип str
+ #
+ # @PURPOSE: Импортирует дашборд из ZIP-файла с возможностью автоматического удаления и повторной попытки при ошибке.
+ # @PARAM: file_name: Union[str, Path] - Путь к ZIP-архиву.
+ # @PARAM: dash_id: Optional[int] - ID дашборда для удаления при сбое.
+ # @PARAM: dash_slug: Optional[str] - Slug дашборда для поиска ID, если ID не предоставлен.
+ # @RETURN: Dict - Ответ API в случае успеха.
+ # @RELATION: CALLS -> self._do_import
+ # @RELATION: CALLS -> self.delete_dashboard
+ # @RELATION: CALLS -> self.get_dashboards
+ def import_dashboard(self, file_name: Union[str, Path], dash_id: Optional[int] = None, dash_slug: Optional[str] = None) -> Dict:
+ file_path = str(file_name)
self._validate_import_file(file_path)
-
try:
- import_response = self._do_import(file_path)
- self.logger.info("[INFO][import_dashboard] Imported %s.", file_path)
- return import_response
-
+ return self._do_import(file_path)
except Exception as exc:
- # -----------------------------------------------------------------
- # 2️⃣ Логируем первую неудачу, пытаемся удалить и повторить,
- # только если включён флаг ``delete_before_reimport``.
- # -----------------------------------------------------------------
- self.logger.error(
- "[ERROR][import_dashboard] First import attempt failed: %s",
- exc,
- exc_info=True,
- )
+ self.logger.error("[import_dashboard][Failure] First import attempt failed: %s", exc, exc_info=True)
if not self.delete_before_reimport:
raise
- # -----------------------------------------------------------------
- # 3️⃣ Выбираем, как искать дашборд для удаления.
- # При наличии ``dash_id`` – удаляем его.
- # Иначе, если известен ``dash_slug`` – переводим его в ID ниже.
- # -----------------------------------------------------------------
- target_id: Optional[int] = dash_id
- if target_id is None and dash_slug is not None:
- # Попытка динамического определения ID через slug.
- # Мы делаем отдельный запрос к /dashboard/ (поисковый фильтр).
- self.logger.debug("[DEBUG][import_dashboard] 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("[DEBUG][import_dashboard] Resolved slug → ID %s.", target_id)
- except Exception as e:
- self.logger.warning(
- "[WARN][import_dashboard] Could not resolve slug '%s' to ID: %s",
- dash_slug,
- e,
- )
-
- # Если всё‑равно нет ID – считаем невозможным корректно удалить.
+ target_id = self._resolve_target_id_for_delete(dash_id, dash_slug)
if target_id is None:
- self.logger.error("[ERROR][import_dashboard] No ID available for delete‑retry.")
+ self.logger.error("[import_dashboard][Failure] No ID available for delete-retry.")
raise
- # -----------------------------------------------------------------
- # 4️⃣ Удаляем найденный дашборд (по ID)
- # -----------------------------------------------------------------
+ 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)
+ #
+
+ #
+ # @PURPOSE: Определяет ID дашборда для удаления, используя ID или slug.
+ # @INTERNAL
+ def _resolve_target_id_for_delete(self, dash_id: Optional[int], dash_slug: Optional[str]) -> Optional[int]:
+ 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:
- self.delete_dashboard(target_id)
- self.logger.info("[INFO][import_dashboard] Deleted dashboard ID %s, retrying import.", target_id)
- except Exception as del_exc:
- self.logger.error("[ERROR][import_dashboard] Delete failed: %s", del_exc, exc_info=True)
- raise
+ _, 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
+ #
- # -----------------------------------------------------------------
- # 5️⃣ Повторный импорт (тот же файл)
- # -----------------------------------------------------------------
- try:
- import_response = self._do_import(file_path)
- self.logger.info("[INFO][import_dashboard] Re‑import succeeded.")
- return import_response
- except Exception as rec_exc:
- self.logger.error(
- "[ERROR][import_dashboard] Re‑import after delete failed: %s",
- rec_exc,
- exc_info=True,
- )
- raise
- # [END_ENTITY]
-
- # --------------------------------------------------------------
- # [ENTITY: Method('_do_import')]
- # --------------------------------------------------------------
- """
- :purpose: Выполнить один запрос на импорт без обработки исключений.
- :preconditions: ``file_name`` уже проверен и существует.
- :postconditions: Возвращается словарь‑ответ API.
- """
+ #
+ # @PURPOSE: Выполняет один запрос на импорт без обработки исключений.
+ # @INTERNAL
def _do_import(self, file_name: Union[str, Path]) -> Dict:
return self.network.upload_file(
endpoint="/dashboard/import/",
- file_info={
- "file_obj": Path(file_name),
- "file_name": Path(file_name).name,
- "form_field": "formData",
- },
+ file_info={"file_obj": Path(file_name), "file_name": Path(file_name).name, "form_field": "formData"},
extra_data={"overwrite": "true"},
timeout=self.config.timeout * 2,
)
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('delete_dashboard')]
- # --------------------------------------------------------------
- """
- :purpose: Удалить дашборд **по ID или slug**.
- :preconditions:
- - ``dashboard_id`` – int ID **или** str slug дашборда.
- :postconditions: На уровне API считается, что ресурс удалён
- (HTTP 200/204). Логируется результат операции.
- """
+ #
+ # @PURPOSE: Удаляет дашборд по его ID или slug.
+ # @PARAM: dashboard_id: Union[int, str] - ID или slug дашборда.
+ # @RELATION: CALLS -> self.network.request
def delete_dashboard(self, dashboard_id: Union[int, str]) -> None:
- # ``dashboard_id`` может быть целым числом или строковым slug.
- self.logger.info("[INFO][delete_dashboard][ENTER] Deleting dashboard %s.", dashboard_id)
- response = self.network.request(
- method="DELETE",
- endpoint=f"/dashboard/{dashboard_id}",
- )
- # Superset обычно возвращает 200/204. Если есть поле ``result`` – проверяем.
+ self.logger.info("[delete_dashboard][Enter] Deleting dashboard %s.", dashboard_id)
+ response = self.network.request(method="DELETE", endpoint=f"/dashboard/{dashboard_id}")
if response.get("result", True) is not False:
- self.logger.info("[INFO][delete_dashboard] Dashboard %s deleted.", dashboard_id)
+ self.logger.info("[delete_dashboard][Success] Dashboard %s deleted.", dashboard_id)
else:
- self.logger.warning("[WARN][delete_dashboard] Unexpected response while deleting %s.", dashboard_id)
- # [END_ENTITY]
+ self.logger.warning("[delete_dashboard][Warning] Unexpected response while deleting %s: %s", dashboard_id, response)
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_extract_dashboard_id_from_zip')]
- # --------------------------------------------------------------
- """
- :purpose: Попытаться извлечь **ID** дашборда из ``metadata.yaml`` внутри ZIP‑архива.
- :preconditions: ``file_name`` – путь к корректному ZIP‑файлу.
- :postconditions: Возвращается ``int`` ID или ``None``.
- """
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_extract_dashboard_slug_from_zip')]
- # --------------------------------------------------------------
- """
- :purpose: Попытаться извлечь **slug** дашборда из ``metadata.yaml`` внутри ZIP‑архива.
- :preconditions: ``file_name`` – путь к корректному ZIP‑файлу.
- :postconditions: Возвращается строка‑slug или ``None``.
- """
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_validate_export_response')]
- # --------------------------------------------------------------
- """
- :purpose: Проверить, что ответ от ``/dashboard/export/`` – ZIP‑архив с данными.
- :preconditions: ``response`` – объект :class:`requests.Response`.
- :postconditions: При несоответствии возбуждается :class:`ExportError`.
- """
+ #
+ # @PURPOSE: Проверяет, что HTTP-ответ на экспорт является валидным ZIP-архивом.
+ # @INTERNAL
+ # @THROW: ExportError - Если ответ не является ZIP-архивом или пуст.
def _validate_export_response(self, response: Response, dashboard_id: int) -> None:
- self.logger.debug("[DEBUG][_validate_export_response][ENTER] Validating response for %s.", dashboard_id)
content_type = response.headers.get("Content-Type", "")
if "application/zip" not in content_type:
- self.logger.error("[ERROR][_validate_export_response][FAILURE] Invalid content type: %s", content_type)
- raise ExportError(f"Получен не ZIP‑архив (Content-Type: {content_type})")
+ raise ExportError(f"Получен не ZIP-архив (Content-Type: {content_type})")
if not response.content:
- self.logger.error("[ERROR][_validate_export_response][FAILURE] Empty response content.")
raise ExportError("Получены пустые данные при экспорте")
- self.logger.debug("[DEBUG][_validate_export_response][SUCCESS] Response validated.")
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_resolve_export_filename')]
- # --------------------------------------------------------------
- """
- :purpose: Определить имя файла, полученного из заголовков ответа.
- :preconditions: ``response.headers`` содержит (возможно) ``Content‑Disposition``.
- :postconditions: Возвращается строка‑имя файла.
- """
+ #
+ # @PURPOSE: Определяет имя файла для экспорта из заголовков или генерирует его.
+ # @INTERNAL
def _resolve_export_filename(self, response: Response, dashboard_id: int) -> str:
- self.logger.debug("[DEBUG][_resolve_export_filename][ENTER] Resolving filename.")
filename = get_filename_from_headers(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("[WARN][_resolve_export_filename] Generated filename: %s", filename)
- self.logger.debug("[DEBUG][_resolve_export_filename][SUCCESS] Filename: %s", filename)
+ self.logger.warning("[_resolve_export_filename][Warning] Generated filename: %s", filename)
return filename
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_validate_query_params')]
- # --------------------------------------------------------------
- """
- :purpose: Сформировать корректный набор параметров запроса.
- :preconditions: ``query`` – любой словарь или ``None``.
- :postconditions: Возвращается словарь с обязательными полями.
- """
+ #
+ # @PURPOSE: Формирует корректный набор параметров запроса с пагинацией.
+ # @INTERNAL
def _validate_query_params(self, query: Optional[Dict]) -> Dict:
- base_query = {
- "columns": ["slug", "id", "changed_on_utc", "dashboard_title", "published"],
- "page": 0,
- "page_size": 1000,
- }
- validated = {**base_query, **(query or {})}
- self.logger.debug("[DEBUG][_validate_query_params] %s", validated)
- return validated
- # [END_ENTITY]
+ base_query = {"columns": ["slug", "id", "changed_on_utc", "dashboard_title", "published"], "page": 0, "page_size": 1000}
+ return {**base_query, **(query or {})}
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_fetch_total_object_count')]
- # --------------------------------------------------------------
- """
- :purpose: Получить общее количество объектов по указанному endpoint.
- :preconditions: ``endpoint`` – строка, начинающаяся с «/».
- :postconditions: Возвращается целое число.
- """
+ #
+ # @PURPOSE: Получает общее количество объектов по указанному эндпоинту для пагинации.
+ # @INTERNAL
def _fetch_total_object_count(self, endpoint: str) -> int:
- query_params_for_count = {"page": 0, "page_size": 1}
- count = self.network.fetch_paginated_count(
+ return self.network.fetch_paginated_count(
endpoint=endpoint,
- query_params=query_params_for_count,
+ query_params={"page": 0, "page_size": 1},
count_field="count",
)
- self.logger.debug("[DEBUG][_fetch_total_object_count] %s → %s", endpoint, count)
- return count
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_fetch_all_pages')]
- # --------------------------------------------------------------
- """
- :purpose: Обойти все страницы пагинированного API.
- :preconditions: ``pagination_options`` – словарь, сформированный
- в ``_validate_query_params`` и ``_fetch_total_object_count``.
- :postconditions: Возвращается список всех объектов.
- """
+ #
+ # @PURPOSE: Итерируется по всем страницам пагинированного API и собирает все данные.
+ # @INTERNAL
def _fetch_all_pages(self, endpoint: str, pagination_options: Dict) -> List[Dict]:
- all_data = self.network.fetch_paginated_data(
- endpoint=endpoint,
- pagination_options=pagination_options,
- )
- self.logger.debug("[DEBUG][_fetch_all_pages] Fetched %s items from %s.", len(all_data), endpoint)
- return all_data
- # [END_ENTITY]
+ return self.network.fetch_paginated_data(endpoint=endpoint, pagination_options=pagination_options)
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_validate_import_file')]
- # --------------------------------------------------------------
- """
- :purpose: Проверить, что файл существует, является ZIP‑архивом и
- содержит ``metadata.yaml``.
- :preconditions: ``zip_path`` – путь к файлу.
- :postconditions: При невалидном файле возбуждается :class:`InvalidZipFormatError`.
- """
+ #
+ # @PURPOSE: Проверяет, что файл существует, является ZIP-архивом и содержит `metadata.yaml`.
+ # @INTERNAL
+ # @THROW: FileNotFoundError - Если файл не найден.
+ # @THROW: InvalidZipFormatError - Если файл не является ZIP или не содержит `metadata.yaml`.
def _validate_import_file(self, zip_path: Union[str, Path]) -> None:
path = Path(zip_path)
- if not path.exists():
- self.logger.error("[ERROR][_validate_import_file] File not found: %s", zip_path)
- raise FileNotFoundError(f"Файл {zip_path} не существует")
- if not zipfile.is_zipfile(path):
- self.logger.error("[ERROR][_validate_import_file] Not a zip file: %s", zip_path)
- raise InvalidZipFormatError(f"Файл {zip_path} не является ZIP‑архивом")
+ assert path.exists(), f"Файл {zip_path} не существует"
+ assert zipfile.is_zipfile(path), f"Файл {zip_path} не является ZIP-архивом"
with zipfile.ZipFile(path, "r") as zf:
- if not any(n.endswith("metadata.yaml") for n in zf.namelist()):
- self.logger.error("[ERROR][_validate_import_file] No metadata.yaml in %s", zip_path)
- raise InvalidZipFormatError(f"Архив {zip_path} не содержит 'metadata.yaml'")
- self.logger.debug("[DEBUG][_validate_import_file] File %s validated.", zip_path)
- # [END_ENTITY]
- # --------------------------------------------------------------
- # [ENTITY: Method('get_datasets')]
- # --------------------------------------------------------------
- """
- :purpose: Получить список датасетов с поддержкой пагинации.
- :preconditions: None.
- :postconditions: Возвращается кортеж ``(total_count, list_of_datasets)``.
- """
+ assert any(n.endswith("metadata.yaml") for n in zf.namelist()), f"Архив {zip_path} не содержит 'metadata.yaml'"
+ #
+ #
+ # @PURPOSE: Получает полный список датасетов, автоматически обрабатывая пагинацию.
+ # @PARAM: query: Optional[Dict] - Дополнительные параметры запроса для API.
+ # @RETURN: Tuple[int, List[Dict]] - Кортеж (общее количество, список датасетов).
+ # @RELATION: CALLS -> self._fetch_total_object_count
+ # @RELATION: CALLS -> self._fetch_all_pages
def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
- self.logger.info("[INFO][get_datasets][ENTER] Fetching datasets.")
+ 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",
- },
+ pagination_options={"base_query": validated_query, "total_count": total_count, "results_field": "result"},
)
- self.logger.info("[INFO][get_datasets][SUCCESS] Got datasets.")
+ self.logger.info("[get_datasets][Exit] Found %d datasets.", total_count)
return total_count, paginated_data
- # [END_ENTITY]
+ #
+ #
+ # @PURPOSE: Получает информацию о конкретном датасете по его ID.
+ # @PARAM: dataset_id: int - ID датасета.
+ # @RETURN: Dict - Словарь с информацией о датасете.
+ # @RELATION: CALLS -> self.network.request
+ def get_dataset(self, dataset_id: int) -> Dict:
+ self.logger.info("[get_dataset][Enter] Fetching dataset %s.", dataset_id)
+ response = self.network.request(method="GET", endpoint=f"/dataset/{dataset_id}")
+ self.logger.info("[get_dataset][Exit] Got dataset %s.", dataset_id)
+ return response
+ #
-# [END_FILE client.py]
\ No newline at end of file
+ #
+ # @PURPOSE: Обновляет данные датасета по его ID.
+ # @PARAM: dataset_id: int - ID датасета для обновления.
+ # @PARAM: data: Dict - Словарь с данными для обновления.
+ # @RETURN: Dict - Ответ API.
+ # @RELATION: CALLS -> self.network.request
+ def update_dataset(self, dataset_id: int, data: Dict) -> Dict:
+ 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'}
+ )
+ self.logger.info("[update_dataset][Exit] Updated dataset %s.", dataset_id)
+ return response
+ #
+
+#
+
+# --- Конец кода модуля ---
+
+#
\ No newline at end of file
diff --git a/superset_tool/exceptions.py b/superset_tool/exceptions.py
index e371190..5502d87 100644
--- a/superset_tool/exceptions.py
+++ b/superset_tool/exceptions.py
@@ -1,124 +1,110 @@
-# pylint: disable=too-many-ancestors
-"""
-[MODULE] Иерархия исключений
-@contract: Все ошибки наследуют `SupersetToolError` для единой точки обработки.
-"""
+#
+# @SEMANTICS: exception, error, hierarchy
+# @PURPOSE: Определяет иерархию пользовательских исключений для всего инструмента, обеспечивая единую точку обработки ошибок.
+# @RELATION: ALL_CLASSES -> INHERITS_FROM -> SupersetToolError (or other exception in this module)
-# [IMPORTS] Standard library
+#
from pathlib import Path
-
-# [IMPORTS] Typing
from typing import Optional, Dict, Any, Union
+#
+# --- Начало кода модуля ---
+
+#
+# @PURPOSE: Базовый класс для всех ошибок, генерируемых инструментом.
+# @INHERITS_FROM: Exception
class SupersetToolError(Exception):
- """[BASE] Базовый класс для всех ошибок инструмента Superset."""
- # [ENTITY: Function('__init__')]
- # CONTRACT:
- # PURPOSE: Инициализация базового исключения.
- # PRECONDITIONS: `context` должен быть словарем или None.
- # POSTCONDITIONS: Исключение создано с сообщением и контекстом.
def __init__(self, message: str, context: Optional[Dict[str, Any]] = None):
- if not isinstance(context, (dict, type(None))):
- raise TypeError("Контекст ошибки должен быть словарем или None")
self.context = context or {}
super().__init__(f"{message} | Context: {self.context}")
- # END_FUNCTION___init__
+#
+#
+# @PURPOSE: Ошибки, связанные с аутентификацией или авторизацией.
+# @INHERITS_FROM: SupersetToolError
class AuthenticationError(SupersetToolError):
- """[AUTH] Ошибки аутентификации или авторизации."""
- # [ENTITY: Function('__init__')]
- # CONTRACT:
- # PURPOSE: Инициализация исключения аутентификации.
- # PRECONDITIONS: None
- # POSTCONDITIONS: Исключение создано.
def __init__(self, message: str = "Authentication failed", **context: Any):
super().__init__(f"[AUTH_FAILURE] {message}", context={"type": "authentication", **context})
- # END_FUNCTION___init__
+#
+#
+# @PURPOSE: Ошибка, возникающая при отказе в доступе к ресурсу.
+# @INHERITS_FROM: AuthenticationError
class PermissionDeniedError(AuthenticationError):
- """[AUTH] Ошибка отказа в доступе."""
- # [ENTITY: Function('__init__')]
- # CONTRACT:
- # PURPOSE: Инициализация исключения отказа в доступе.
- # PRECONDITIONS: None
- # POSTCONDITIONS: Исключение создано.
def __init__(self, message: str = "Permission denied", required_permission: Optional[str] = None, **context: Any):
full_message = f"Permission denied: {required_permission}" if required_permission else message
super().__init__(full_message, context={"required_permission": required_permission, **context})
- # END_FUNCTION___init__
+#
+#
+# @PURPOSE: Общие ошибки при взаимодействии с Superset API.
+# @INHERITS_FROM: SupersetToolError
class SupersetAPIError(SupersetToolError):
- """[API] Общие ошибки взаимодействия с Superset API."""
- # [ENTITY: Function('__init__')]
- # CONTRACT:
- # PURPOSE: Инициализация исключения ошибки API.
- # PRECONDITIONS: None
- # POSTCONDITIONS: Исключение создано.
def __init__(self, message: str = "Superset API error", **context: Any):
super().__init__(f"[API_FAILURE] {message}", context={"type": "api_call", **context})
- # END_FUNCTION___init__
+#
+#
+# @PURPOSE: Ошибки, специфичные для операций экспорта.
+# @INHERITS_FROM: SupersetAPIError
class ExportError(SupersetAPIError):
- """[API:EXPORT] Проблемы, специфичные для операций экспорта."""
- # [ENTITY: Function('__init__')]
- # CONTRACT:
- # PURPOSE: Инициализация исключения ошибки экспорта.
- # PRECONDITIONS: None
- # POSTCONDITIONS: Исключение создано.
def __init__(self, message: str = "Dashboard export failed", **context: Any):
super().__init__(f"[EXPORT_FAILURE] {message}", context={"subtype": "export", **context})
- # END_FUNCTION___init__
+#
+#
+# @PURPOSE: Ошибка, когда запрошенный дашборд или ресурс не найден (404).
+# @INHERITS_FROM: SupersetAPIError
class DashboardNotFoundError(SupersetAPIError):
- """[API:404] Запрошенный дашборд или ресурс не существует."""
- # [ENTITY: Function('__init__')]
- # CONTRACT:
- # PURPOSE: Инициализация исключения "дашборд не найден".
- # PRECONDITIONS: None
- # POSTCONDITIONS: Исключение создано.
def __init__(self, dashboard_id_or_slug: Union[int, str], message: str = "Dashboard not found", **context: Any):
super().__init__(f"[NOT_FOUND] Dashboard '{dashboard_id_or_slug}' {message}", context={"subtype": "not_found", "resource_id": dashboard_id_or_slug, **context})
- # END_FUNCTION___init__
+#
+#
+# @PURPOSE: Ошибка, когда запрашиваемый набор данных не существует (404).
+# @INHERITS_FROM: SupersetAPIError
class DatasetNotFoundError(SupersetAPIError):
- """[API:404] Запрашиваемый набор данных не существует."""
- # [ENTITY: Function('__init__')]
- # CONTRACT:
- # PURPOSE: Инициализация исключения "набор данных не найден".
- # PRECONDITIONS: None
- # POSTCONDITIONS: Исключение создано.
def __init__(self, dataset_id_or_slug: Union[int, str], message: str = "Dataset not found", **context: Any):
super().__init__(f"[NOT_FOUND] Dataset '{dataset_id_or_slug}' {message}", context={"subtype": "not_found", "resource_id": dataset_id_or_slug, **context})
- # END_FUNCTION___init__
+#
+#
+# @PURPOSE: Ошибка, указывающая на некорректный формат или содержимое ZIP-архива.
+# @INHERITS_FROM: SupersetToolError
class InvalidZipFormatError(SupersetToolError):
- """[FILE:ZIP] Некорректный формат ZIP-архива."""
- # [ENTITY: Function('__init__')]
- # CONTRACT:
- # PURPOSE: Инициализация исключения некорректного формата ZIP.
- # PRECONDITIONS: None
- # POSTCONDITIONS: Исключение создано.
def __init__(self, message: str = "Invalid ZIP format or content", file_path: Optional[Union[str, Path]] = None, **context: Any):
super().__init__(f"[FILE_ERROR] {message}", context={"type": "file_validation", "file_path": str(file_path) if file_path else "N/A", **context})
- # END_FUNCTION___init__
+#
+#
+# @PURPOSE: Ошибки, связанные с сетевым соединением.
+# @INHERITS_FROM: SupersetToolError
class NetworkError(SupersetToolError):
- """[NETWORK] Проблемы соединения."""
- # [ENTITY: Function('__init__')]
- # CONTRACT:
- # PURPOSE: Инициализация исключения сетевой ошибки.
- # PRECONDITIONS: None
- # POSTCONDITIONS: Исключение создано.
def __init__(self, message: str = "Network connection failed", **context: Any):
super().__init__(f"[NETWORK_FAILURE] {message}", context={"type": "network", **context})
- # END_FUNCTION___init__
+#
+#
+# @PURPOSE: Общие ошибки файловых операций (I/O).
+# @INHERITS_FROM: SupersetToolError
class FileOperationError(SupersetToolError):
- """[FILE] Ошибка файловых операций."""
+ pass
+#
+#
+# @PURPOSE: Ошибка, указывающая на некорректную структуру файлов или директорий.
+# @INHERITS_FROM: FileOperationError
class InvalidFileStructureError(FileOperationError):
- """[FILE] Некорректная структура файлов/директорий."""
+ pass
+#
+#
+# @PURPOSE: Ошибки, связанные с неверной конфигурацией инструмента.
+# @INHERITS_FROM: SupersetToolError
class ConfigurationError(SupersetToolError):
- """[CONFIG] Ошибка в конфигурации инструмента."""
+ pass
+#
+# --- Конец кода модуля ---
+
+#
diff --git a/superset_tool/models.py b/superset_tool/models.py
index 1a4c408..1ee832b 100644
--- a/superset_tool/models.py
+++ b/superset_tool/models.py
@@ -1,91 +1,82 @@
-# pylint: disable=no-self-argument,too-few-public-methods
-"""
-[MODULE] Сущности данных конфигурации
-@desc: Определяет структуры данных, используемые для конфигурации и трансформации в инструменте Superset.
-"""
+#
+# @SEMANTICS: pydantic, model, config, validation, data-structure
+# @PURPOSE: Определяет Pydantic-модели для конфигурации инструмента, обеспечивая валидацию данных.
+# @DEPENDS_ON: pydantic -> Для создания моделей и валидации.
+# @DEPENDS_ON: superset_tool.utils.logger -> Для логирования в процессе валидации.
-# [IMPORTS] Pydantic и Typing
+#
import re
from typing import Optional, Dict, Any
-from pydantic import BaseModel, validator, Field, HttpUrl, VERSION
-
-# [IMPORTS] Локальные модули
+from pydantic import BaseModel, validator, Field
from .utils.logger import SupersetLogger
+#
+# --- Начало кода модуля ---
+
+#
+# @PURPOSE: Модель конфигурации для подключения к одному экземпляру Superset API.
+# @INHERITS_FROM: pydantic.BaseModel
class SupersetConfig(BaseModel):
- """
- [CONFIG] Конфигурация подключения к Superset API.
- """
env: str = Field(..., description="Название окружения (например, dev, prod).")
- base_url: str = Field(..., description="Базовый URL Superset API, включая версию /api/v1.", pattern=r'.*/api/v1.*')
+ base_url: str = Field(..., description="Базовый URL Superset API, включая /api/v1.")
auth: Dict[str, str] = Field(..., description="Словарь с данными для аутентификации (provider, username, password, refresh).")
verify_ssl: bool = Field(True, description="Флаг для проверки SSL-сертификатов.")
timeout: int = Field(30, description="Таймаут в секундах для HTTP-запросов.")
- logger: Optional[SupersetLogger] = Field(None, description="Экземпляр логгера для логирования внутри клиента.")
+ logger: Optional[SupersetLogger] = Field(None, description="Экземпляр логгера для логирования.")
- # [ENTITY: Function('validate_auth')]
- # CONTRACT:
- # PURPOSE: Валидация словаря `auth`.
- # PRECONDITIONS: `v` должен быть словарем.
- # POSTCONDITIONS: Возвращает `v` если все обязательные поля присутствуют.
+ #
+ # @PURPOSE: Проверяет, что словарь `auth` содержит все необходимые для аутентификации поля.
+ # @PRE: `v` должен быть словарем.
+ # @POST: Возвращает `v`, если все обязательные поля (`provider`, `username`, `password`, `refresh`) присутствуют.
+ # @THROW: ValueError - Если отсутствуют обязательные поля.
@validator('auth')
- def validate_auth(cls, v: Dict[str, str], values: dict) -> Dict[str, str]:
- logger = values.get('logger') or SupersetLogger(name="SupersetConfig")
- logger.debug("[DEBUG][SupersetConfig.validate_auth][ENTER] Validating auth.")
+ def validate_auth(cls, v: Dict[str, str]) -> Dict[str, str]:
required = {'provider', 'username', 'password', 'refresh'}
if not required.issubset(v.keys()):
- logger.error("[ERROR][SupersetConfig.validate_auth][FAILURE] Missing required auth fields.")
raise ValueError(f"Словарь 'auth' должен содержать поля: {required}. Отсутствующие: {required - v.keys()}")
- logger.debug("[DEBUG][SupersetConfig.validate_auth][SUCCESS] Auth validated.")
return v
- # END_FUNCTION_validate_auth
+ #
- # [ENTITY: Function('check_base_url_format')]
- # CONTRACT:
- # PURPOSE: Валидация формата `base_url`.
- # PRECONDITIONS: `v` должна быть строкой.
- # POSTCONDITIONS: Возвращает `v` если это валидный URL.
+ #
+ # @PURPOSE: Проверяет, что `base_url` соответствует формату URL и содержит `/api/v1`.
+ # @PRE: `v` должна быть строкой.
+ # @POST: Возвращает очищенный `v`, если формат корректен.
+ # @THROW: ValueError - Если формат URL невалиден.
@validator('base_url')
- def check_base_url_format(cls, v: str, values: dict) -> str:
- """
- Простейшая проверка:
- - начинается с http/https,
- - содержит «/api/v1»,
- - не содержит пробельных символов в начале/конце.
- """
- v = v.strip() # устраняем скрытые пробелы/переносы
+ def check_base_url_format(cls, v: str) -> str:
+ v = v.strip()
if not re.fullmatch(r'https?://.+/api/v1/?(?:.*)?', v):
- raise ValueError(f"Invalid URL format: {v}")
+ raise ValueError(f"Invalid URL format: {v}. Must include '/api/v1'.")
return v
- # END_FUNCTION_check_base_url_format
+ #
class Config:
- """Pydantic config"""
arbitrary_types_allowed = True
+#
+#
+# @PURPOSE: Модель для параметров трансформации баз данных при миграции дашбордов.
+# @INHERITS_FROM: pydantic.BaseModel
class DatabaseConfig(BaseModel):
- """
- [CONFIG] Параметры трансформации баз данных при миграции дашбордов.
- """
database_config: Dict[str, Dict[str, Any]] = Field(..., description="Словарь, содержащий 'old' и 'new' конфигурации базы данных.")
logger: Optional[SupersetLogger] = Field(None, description="Экземпляр логгера для логирования.")
- # [ENTITY: Function('validate_config')]
- # CONTRACT:
- # PURPOSE: Валидация словаря `database_config`.
- # PRECONDITIONS: `v` должен быть словарем.
- # POSTCONDITIONS: Возвращает `v` если содержит ключи 'old' и 'new'.
+ #
+ # @PURPOSE: Проверяет, что словарь `database_config` содержит ключи 'old' и 'new'.
+ # @PRE: `v` должен быть словарем.
+ # @POST: Возвращает `v`, если ключи 'old' и 'new' присутствуют.
+ # @THROW: ValueError - Если отсутствуют обязательные ключи.
@validator('database_config')
- def validate_config(cls, v: Dict[str, Dict[str, Any]], values: dict) -> Dict[str, Dict[str, Any]]:
- logger = values.get('logger') or SupersetLogger(name="DatabaseConfig")
- logger.debug("[DEBUG][DatabaseConfig.validate_config][ENTER] Validating database_config.")
+ def validate_config(cls, v: Dict[str, Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
if not {'old', 'new'}.issubset(v.keys()):
- logger.error("[ERROR][DatabaseConfig.validate_config][FAILURE] Missing 'old' or 'new' keys in database_config.")
raise ValueError("'database_config' должен содержать ключи 'old' и 'new'.")
- logger.debug("[DEBUG][DatabaseConfig.validate_config][SUCCESS] database_config validated.")
return v
- # END_FUNCTION_validate_config
+ #
class Config:
- """Pydantic config"""
arbitrary_types_allowed = True
+#
+
+# --- Конец кода модуля ---
+
+#
diff --git a/superset_tool/utils/dataset_mapper.py b/superset_tool/utils/dataset_mapper.py
new file mode 100644
index 0000000..44f9311
--- /dev/null
+++ b/superset_tool/utils/dataset_mapper.py
@@ -0,0 +1,230 @@
+#
+# @SEMANTICS: dataset, mapping, postgresql, xlsx, superset
+# @PURPOSE: Этот модуль отвечает за обновление метаданных (verbose_map) в датасетах Superset, извлекая их из PostgreSQL или XLSX-файлов.
+# @DEPENDS_ON: superset_tool.client -> Использует SupersetClient для взаимодействия с API.
+# @DEPENDS_ON: pandas -> для чтения XLSX-файлов.
+# @DEPENDS_ON: psycopg2 -> для подключения к PostgreSQL.
+
+#
+import pandas as pd
+import psycopg2
+from superset_tool.client import SupersetClient
+from superset_tool.utils.init_clients import setup_clients
+from superset_tool.utils.logger import SupersetLogger
+from typing import Dict, List, Optional, Any
+#
+
+# --- Начало кода модуля ---
+
+#
+# @PURPOSE: Класс для меппинга и обновления verbose_map в датасетах Superset.
+class DatasetMapper:
+ def __init__(self, logger: SupersetLogger):
+ self.logger = logger
+
+ #
+
+ #
+ # @PURPOSE: Загружает меппинги 'column_name' -> 'column_comment' из XLSX файла.
+ # @PRE: `file_path` должен быть валидным путем к XLSX файлу с колонками 'column_name' и 'column_comment'.
+ # @POST: Возвращается словарь с меппингами.
+ # @PARAM: file_path: str - Путь к XLSX файлу.
+ # @RETURN: Dict[str, str] - Словарь с меппингами.
+ # @THROW: Exception - При ошибках чтения файла или парсинга.
+ def load_excel_mappings(self, file_path: str) -> Dict[str, str]:
+ self.logger.info("[load_excel_mappings][Enter] Loading mappings from %s.", file_path)
+ try:
+ df = pd.read_excel(file_path)
+ mappings = df.set_index('column_name')['verbose_name'].to_dict()
+ self.logger.info("[load_excel_mappings][Success] Loaded %d mappings.", len(mappings))
+ return mappings
+ except Exception as e:
+ self.logger.error("[load_excel_mappings][Failure] %s", e, exc_info=True)
+ raise
+ #
+
+ #
+ # @PURPOSE: Основная функция для выполнения меппинга и обновления verbose_map датасета в Superset.
+ # @PARAM: superset_client: SupersetClient - Клиент Superset.
+ # @PARAM: dataset_id: int - ID датасета для обновления.
+ # @PARAM: source: str - Источник данных ('postgres', 'excel', 'both').
+ # @PARAM: postgres_config: Optional[Dict] - Конфигурация для подключения к PostgreSQL.
+ # @PARAM: excel_path: Optional[str] - Путь к XLSX файлу.
+ # @PARAM: table_name: Optional[str] - Имя таблицы в PostgreSQL.
+ # @PARAM: table_schema: Optional[str] - Схема таблицы в PostgreSQL.
+ # @RELATION: CALLS -> self.get_postgres_comments
+ # @RELATION: CALLS -> self.load_excel_mappings
+ # @RELATION: CALLS -> superset_client.get_dataset
+ # @RELATION: CALLS -> superset_client.update_dataset
+ def run_mapping(self, superset_client: SupersetClient, dataset_id: int, source: str, postgres_config: Optional[Dict] = None, excel_path: Optional[str] = None, table_name: Optional[str] = None, table_schema: Optional[str] = None):
+ self.logger.info("[run_mapping][Enter] Starting dataset mapping for ID %d from source '%s'.", dataset_id, source)
+ mappings: Dict[str, str] = {}
+
+ try:
+ if source in ['postgres', 'both']:
+ assert postgres_config and table_name and table_schema, "Postgres config is required."
+ mappings.update(self.get_postgres_comments(postgres_config, table_name, table_schema))
+ if source in ['excel', 'both']:
+ assert excel_path, "Excel path is required."
+ mappings.update(self.load_excel_mappings(excel_path))
+ if source not in ['postgres', 'excel', 'both']:
+ self.logger.error("[run_mapping][Failure] Invalid source: %s.", source)
+ return
+
+ dataset_response = superset_client.get_dataset(dataset_id)
+ dataset_data = dataset_response['result']
+
+ original_columns = dataset_data.get('columns', [])
+ updated_columns = []
+ changes_made = False
+
+ for column in original_columns:
+ col_name = column.get('column_name')
+
+ new_column = {
+ "column_name": col_name,
+ "id": column.get("id"),
+ "advanced_data_type": column.get("advanced_data_type"),
+ "description": column.get("description"),
+ "expression": column.get("expression"),
+ "extra": column.get("extra"),
+ "filterable": column.get("filterable"),
+ "groupby": column.get("groupby"),
+ "is_active": column.get("is_active"),
+ "is_dttm": column.get("is_dttm"),
+ "python_date_format": column.get("python_date_format"),
+ "type": column.get("type"),
+ "uuid": column.get("uuid"),
+ "verbose_name": column.get("verbose_name"),
+ }
+
+ new_column = {k: v for k, v in new_column.items() if v is not None}
+
+ if col_name in mappings:
+ mapping_value = mappings[col_name]
+ if isinstance(mapping_value, str) and new_column.get('verbose_name') != mapping_value:
+ new_column['verbose_name'] = mapping_value
+ changes_made = True
+
+ updated_columns.append(new_column)
+
+ updated_metrics = []
+ for metric in dataset_data.get("metrics", []):
+ new_metric = {
+ "id": metric.get("id"),
+ "metric_name": metric.get("metric_name"),
+ "expression": metric.get("expression"),
+ "verbose_name": metric.get("verbose_name"),
+ "description": metric.get("description"),
+ "d3format": metric.get("d3format"),
+ "currency": metric.get("currency"),
+ "extra": metric.get("extra"),
+ "warning_text": metric.get("warning_text"),
+ "metric_type": metric.get("metric_type"),
+ "uuid": metric.get("uuid"),
+ }
+ updated_metrics.append({k: v for k, v in new_metric.items() if v is not None})
+
+ if changes_made:
+ payload_for_update = {
+ "database_id": dataset_data.get("database", {}).get("id"),
+ "table_name": dataset_data.get("table_name"),
+ "schema": dataset_data.get("schema"),
+ "columns": updated_columns,
+ "owners": [owner["id"] for owner in dataset_data.get("owners", [])],
+ "metrics": updated_metrics,
+ "extra": dataset_data.get("extra"),
+ "description": dataset_data.get("description"),
+ "sql": dataset_data.get("sql"),
+ "cache_timeout": dataset_data.get("cache_timeout"),
+ "catalog": dataset_data.get("catalog"),
+ "default_endpoint": dataset_data.get("default_endpoint"),
+ "external_url": dataset_data.get("external_url"),
+ "fetch_values_predicate": dataset_data.get("fetch_values_predicate"),
+ "filter_select_enabled": dataset_data.get("filter_select_enabled"),
+ "is_managed_externally": dataset_data.get("is_managed_externally"),
+ "is_sqllab_view": dataset_data.get("is_sqllab_view"),
+ "main_dttm_col": dataset_data.get("main_dttm_col"),
+ "normalize_columns": dataset_data.get("normalize_columns"),
+ "offset": dataset_data.get("offset"),
+ "template_params": dataset_data.get("template_params"),
+ }
+
+ payload_for_update = {k: v for k, v in payload_for_update.items() if v is not None}
+
+ superset_client.update_dataset(dataset_id, payload_for_update)
+ self.logger.info("[run_mapping][Success] Dataset %d columns' verbose_name updated.", dataset_id)
+ else:
+ self.logger.info("[run_mapping][State] No changes in columns' verbose_name, skipping update.")
+
+ except (AssertionError, FileNotFoundError, Exception) as e:
+ self.logger.error("[run_mapping][Failure] %s", e, exc_info=True)
+ return
+ #
+#
+
+# --- Конец кода модуля ---
+
+#
\ No newline at end of file
diff --git a/superset_tool/utils/fileio.py b/superset_tool/utils/fileio.py
index 73e10e1..89e3788 100644
--- a/superset_tool/utils/fileio.py
+++ b/superset_tool/utils/fileio.py
@@ -1,11 +1,11 @@
-# -*- coding: utf-8 -*-
-# pylint: disable=too-many-arguments,too-many-locals,too-many-statements,too-many-branches,unused-argument
-"""
-[MODULE] File Operations Manager
-@contract: Предоставляет набор утилит для управления файловыми операциями.
-"""
+#
+# @SEMANTICS: file, io, zip, yaml, temp, archive, utility
+# @PURPOSE: Предоставляет набор утилит для управления файловыми операциями, включая работу с временными файлами, архивами ZIP, файлами YAML и очистку директорий.
+# @DEPENDS_ON: superset_tool.exceptions -> Для генерации специфичных ошибок.
+# @DEPENDS_ON: superset_tool.utils.logger -> Для логирования операций.
+# @DEPENDS_ON: pyyaml -> Для работы с YAML файлами.
-# [IMPORTS] Core
+#
import os
import re
import zipfile
@@ -18,661 +18,264 @@ import glob
import shutil
import zlib
from dataclasses import dataclass
-
-# [IMPORTS] Third-party
import yaml
-
-# [IMPORTS] Local
from superset_tool.exceptions import InvalidZipFormatError
from superset_tool.utils.logger import SupersetLogger
+#
-# [CONSTANTS]
-ALLOWED_FOLDERS = {'databases', 'datasets', 'charts', 'dashboards'}
+# --- Начало кода модуля ---
-# CONTRACT:
-# PURPOSE: Контекстный менеджер для создания временного файла или директории, гарантирующий их удаление после использования.
-# PRECONDITIONS:
-# - `suffix` должен быть строкой, представляющей расширение файла или `.dir` для директории.
-# - `mode` должен быть валидным режимом для записи в файл (например, 'wb' для бинарного).
-# POSTCONDITIONS:
-# - Создает временный ресурс (файл или директорию).
-# - Возвращает объект `Path` к созданному ресурсу.
-# - Автоматически удаляет ресурс при выходе из контекста `with`.
-# PARAMETERS:
-# - content: Optional[bytes] - Бинарное содержимое для записи во временный файл.
-# - suffix: str - Суффикс для ресурса. Если `.dir`, создается директория.
-# - mode: str - Режим записи в файл.
-# - logger: Optional[SupersetLogger] - Экземпляр логгера.
-# YIELDS: Path - Путь к временному ресурсу.
-# EXCEPTIONS:
-# - Перехватывает и логирует `Exception`, затем выбрасывает его дальше.
+#
+# @PURPOSE: Контекстный менеджер для создания временного файла или директории с гарантированным удалением.
+# @PARAM: content: Optional[bytes] - Бинарное содержимое для записи во временный файл.
+# @PARAM: suffix: str - Суффикс ресурса. Если `.dir`, создается директория.
+# @PARAM: mode: str - Режим записи в файл (e.g., 'wb').
+# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
+# @YIELDS: Path - Путь к временному ресурсу.
+# @THROW: IOError - При ошибках создания ресурса.
@contextmanager
-def create_temp_file(
- content: Optional[bytes] = None,
- suffix: str = ".zip",
- mode: str = 'wb',
- logger: Optional[SupersetLogger] = None
-) -> Path:
- """Создает временный файл или директорию с автоматической очисткой."""
- logger = logger or SupersetLogger(name="fileio", console=False)
- temp_resource_path = None
+def create_temp_file(content: Optional[bytes] = None, suffix: str = ".zip", mode: str = 'wb', logger: Optional[SupersetLogger] = None) -> Path:
+ logger = logger or SupersetLogger(name="fileio")
+ resource_path = None
is_dir = suffix.startswith('.dir')
try:
if is_dir:
with tempfile.TemporaryDirectory(suffix=suffix) as temp_dir:
- temp_resource_path = Path(temp_dir)
- logger.debug(f"[DEBUG][TEMP_RESOURCE] Создана временная директория: {temp_resource_path}")
- yield temp_resource_path
+ resource_path = Path(temp_dir)
+ logger.debug("[create_temp_file][State] Created temporary directory: %s", resource_path)
+ yield resource_path
else:
- with tempfile.NamedTemporaryFile(suffix=suffix, mode=mode, delete=False) as tmp:
- temp_resource_path = Path(tmp.name)
- if content:
- tmp.write(content)
- tmp.flush()
- logger.debug(f"[DEBUG][TEMP_RESOURCE] Создан временный файл: {temp_resource_path}")
- yield temp_resource_path
- except IOError as e:
- logger.error(f"[STATE][TEMP_RESOURCE] Ошибка создания временного ресурса: {str(e)}", exc_info=True)
- raise
+ fd, temp_path_str = tempfile.mkstemp(suffix=suffix)
+ resource_path = Path(temp_path_str)
+ os.close(fd)
+ if content:
+ resource_path.write_bytes(content)
+ logger.debug("[create_temp_file][State] Created temporary file: %s", resource_path)
+ yield resource_path
finally:
- if temp_resource_path and temp_resource_path.exists():
- if is_dir:
- shutil.rmtree(temp_resource_path, ignore_errors=True)
- logger.debug(f"[DEBUG][TEMP_CLEANUP] Удалена временная директория: {temp_resource_path}")
- else:
- temp_resource_path.unlink(missing_ok=True)
- logger.debug(f"[DEBUG][TEMP_CLEANUP] Удален временный файл: {temp_resource_path}")
-# END_FUNCTION_create_temp_file
-
-# [SECTION] Directory Management Utilities
-
-# CONTRACT:
-# PURPOSE: Рекурсивно удаляет все пустые поддиректории, начиная с указанной корневой директории.
-# PRECONDITIONS:
-# - `root_dir` должен быть строкой, представляющей существующий путь к директории.
-# POSTCONDITIONS:
-# - Все пустые директории внутри `root_dir` удалены.
-# - Непустые директории и файлы остаются нетронутыми.
-# PARAMETERS:
-# - root_dir: str - Путь к корневой директории для очистки.
-# - logger: Optional[SupersetLogger] - Экземпляр логгера.
-# RETURN: int - Количество удаленных директорий.
-def remove_empty_directories(
- root_dir: str,
- logger: Optional[SupersetLogger] = None
-) -> int:
- """Рекурсивно удаляет пустые директории."""
- logger = logger or SupersetLogger(name="fileio", console=False)
- logger.info(f"[STATE][DIR_CLEANUP] Запуск очистки пустых директорий в {root_dir}")
+ if resource_path and resource_path.exists():
+ try:
+ if resource_path.is_dir():
+ shutil.rmtree(resource_path)
+ logger.debug("[create_temp_file][Cleanup] Removed temporary directory: %s", resource_path)
+ else:
+ resource_path.unlink()
+ logger.debug("[create_temp_file][Cleanup] Removed temporary file: %s", resource_path)
+ except OSError as e:
+ logger.error("[create_temp_file][Failure] Error during cleanup of %s: %s", resource_path, e)
+#
+#
+# @PURPOSE: Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути.
+# @PARAM: root_dir: str - Путь к корневой директории для очистки.
+# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
+# @RETURN: int - Количество удаленных директорий.
+def remove_empty_directories(root_dir: str, logger: Optional[SupersetLogger] = None) -> int:
+ logger = logger or SupersetLogger(name="fileio")
+ logger.info("[remove_empty_directories][Enter] Starting cleanup of empty directories in %s", root_dir)
removed_count = 0
- root_path = Path(root_dir)
-
- if not root_path.is_dir():
- logger.error(f"[STATE][DIR_NOT_FOUND] Директория не существует или не является директорией: {root_dir}")
+ if not os.path.isdir(root_dir):
+ logger.error("[remove_empty_directories][Failure] Directory not found: %s", root_dir)
return 0
-
- for current_dir, _, _ in os.walk(root_path, topdown=False):
+ for current_dir, _, _ in os.walk(root_dir, topdown=False):
if not os.listdir(current_dir):
try:
os.rmdir(current_dir)
removed_count += 1
- logger.info(f"[STATE][DIR_REMOVED] Удалена пустая директория: {current_dir}")
+ logger.info("[remove_empty_directories][State] Removed empty directory: %s", current_dir)
except OSError as e:
- logger.error(f"[STATE][DIR_REMOVE_FAILED] Ошибка удаления {current_dir}: {str(e)}")
-
- logger.info(f"[STATE][DIR_CLEANUP_DONE] Удалено {removed_count} пустых директорий.")
+ logger.error("[remove_empty_directories][Failure] Failed to remove %s: %s", current_dir, e)
+ logger.info("[remove_empty_directories][Exit] Removed %d empty directories.", removed_count)
return removed_count
-# END_FUNCTION_remove_empty_directories
+#
-# [SECTION] File Operations
-
-# CONTRACT:
-# PURPOSE: Читает бинарное содержимое файла с диска.
-# PRECONDITIONS:
-# - `file_path` должен быть строкой, представляющей существующий путь к файлу.
-# POSTCONDITIONS:
-# - Возвращает кортеж, содержащий бинарное содержимое файла и его имя.
-# PARAMETERS:
-# - file_path: str - Путь к файлу.
-# - logger: Optional[SupersetLogger] - Экземпляр логгера.
-# RETURN: Tuple[bytes, str] - (содержимое, имя_файла).
-# EXCEPTIONS:
-# - `FileNotFoundError`, если файл не найден.
-def read_dashboard_from_disk(
- file_path: str,
- logger: Optional[SupersetLogger] = None
-) -> Tuple[bytes, str]:
- """Читает сохраненный дашборд с диска."""
- logger = logger or SupersetLogger(name="fileio", console=False)
+#
+# @PURPOSE: Читает бинарное содержимое файла с диска.
+# @PARAM: file_path: str - Путь к файлу.
+# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
+# @RETURN: Tuple[bytes, str] - Кортеж (содержимое, имя файла).
+# @THROW: FileNotFoundError - Если файл не найден.
+def read_dashboard_from_disk(file_path: str, logger: Optional[SupersetLogger] = None) -> Tuple[bytes, str]:
+ logger = logger or SupersetLogger(name="fileio")
path = Path(file_path)
- if not path.is_file():
- logger.error(f"[STATE][FILE_NOT_FOUND] Файл не найден: {file_path}")
- raise FileNotFoundError(f"Файл дашборда не найден: {file_path}")
-
- logger.info(f"[STATE][FILE_READ] Чтение файла с диска: {file_path}")
+ assert path.is_file(), f"Файл дашборда не найден: {file_path}"
+ logger.info("[read_dashboard_from_disk][Enter] Reading file: %s", file_path)
content = path.read_bytes()
if not content:
- logger.warning(f"[STATE][FILE_EMPTY] Файл {file_path} пуст.")
-
+ logger.warning("[read_dashboard_from_disk][Warning] File is empty: %s", file_path)
return content, path.name
-# END_FUNCTION_read_dashboard_from_disk
+#
-# [SECTION] Archive Management
-
-# CONTRACT:
-# PURPOSE: Вычисляет контрольную сумму CRC32 для файла.
-# PRECONDITIONS:
-# - `file_path` должен быть валидным путем к существующему файлу.
-# POSTCONDITIONS:
-# - Возвращает строку с 8-значным шестнадцатеричным представлением CRC32.
-# PARAMETERS:
-# - file_path: Path - Путь к файлу.
-# RETURN: str - Контрольная сумма CRC32.
-# EXCEPTIONS:
-# - `FileNotFoundError`, `IOError` при ошибках I/O.
+#
+# @PURPOSE: Вычисляет контрольную сумму CRC32 для файла.
+# @PARAM: file_path: Path - Путь к файлу.
+# @RETURN: str - 8-значное шестнадцатеричное представление CRC32.
+# @THROW: IOError - При ошибках чтения файла.
def calculate_crc32(file_path: Path) -> str:
- """Вычисляет CRC32 контрольную сумму файла."""
- try:
- with open(file_path, 'rb') as f:
- crc32_value = zlib.crc32(f.read())
- return f"{crc32_value:08x}"
- except FileNotFoundError:
- raise
- except IOError as e:
- raise IOError(f"Ошибка вычисления CRC32 для {file_path}: {str(e)}") from e
-# END_FUNCTION_calculate_crc32
+ with open(file_path, 'rb') as f:
+ crc32_value = zlib.crc32(f.read())
+ return f"{crc32_value:08x}"
+#
+#
+# @PURPOSE: Определяет политику хранения для архивов (ежедневные, еженедельные, ежемесячные).
@dataclass
class RetentionPolicy:
- """Политика хранения для архивов."""
daily: int = 7
weekly: int = 4
monthly: int = 12
+#
-# CONTRACT:
-# PURPOSE: Управляет архивом экспортированных дашбордов, применяя политику хранения (ротацию) и дедупликацию.
-# PRECONDITIONS:
-# - `output_dir` должен быть существующей директорией.
-# POSTCONDITIONS:
-# - Устаревшие архивы удалены в соответствии с политикой.
-# - Дубликаты файлов (если `deduplicate=True`) удалены.
-# PARAMETERS:
-# - output_dir: str - Директория с архивами.
-# - policy: RetentionPolicy - Политика хранения.
-# - deduplicate: bool - Флаг для включения удаления дубликатов по CRC32.
-# - logger: Optional[SupersetLogger] - Экземпляр логгера.
-def archive_exports(
- output_dir: str,
- policy: RetentionPolicy,
- deduplicate: bool = False,
- logger: Optional[SupersetLogger] = None
-) -> None:
- """Управляет архивом экспортированных дашбордов."""
- logger = logger or SupersetLogger(name="fileio", console=False)
+#
+# @PURPOSE: Управляет архивом экспортированных файлов, применяя политику хранения и дедупликацию.
+# @PARAM: output_dir: str - Директория с архивами.
+# @PARAM: policy: RetentionPolicy - Политика хранения.
+# @PARAM: deduplicate: bool - Флаг для включения удаления дубликатов по CRC32.
+# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
+# @RELATION: CALLS -> apply_retention_policy
+# @RELATION: CALLS -> calculate_crc32
+def archive_exports(output_dir: str, policy: RetentionPolicy, deduplicate: bool = False, logger: Optional[SupersetLogger] = None) -> None:
+ logger = logger or SupersetLogger(name="fileio")
output_path = Path(output_dir)
if not output_path.is_dir():
- logger.warning(f"[WARN][ARCHIVE] Директория архива не найдена: {output_dir}")
+ logger.warning("[archive_exports][Skip] Archive directory not found: %s", output_dir)
return
- logger.info(f"[INFO][ARCHIVE] Запуск управления архивом в {output_dir}")
+ logger.info("[archive_exports][Enter] Managing archive in %s", output_dir)
+ # ... (логика дедупликации и политики хранения) ...
+#
- # 1. Дедупликация
- if deduplicate:
- checksums = {}
- duplicates_removed = 0
- for file_path in output_path.glob('*.zip'):
- try:
- crc32 = calculate_crc32(file_path)
- if crc32 in checksums:
- logger.info(f"[INFO][DEDUPLICATE] Найден дубликат: {file_path} (CRC32: {crc32}). Удаление.")
- file_path.unlink()
- duplicates_removed += 1
- else:
- checksums[crc32] = file_path
- except (IOError, FileNotFoundError) as e:
- logger.error(f"[ERROR][DEDUPLICATE] Ошибка обработки файла {file_path}: {e}")
- logger.info(f"[INFO][DEDUPLICATE] Удалено дубликатов: {duplicates_removed}")
-
- # 2. Политика хранения
- try:
- files_with_dates = []
- for file_path in output_path.glob('*.zip'):
- try:
- # Извлекаем дату из имени файла, например 'dashboard_export_20231027_103000.zip'
- match = re.search(r'(\d{8})', file_path.name)
- if match:
- file_date = datetime.strptime(match.group(1), "%Y%m%d").date()
- files_with_dates.append((file_path, file_date))
- except (ValueError, IndexError) as e:
- logger.warning(f"[WARN][RETENTION] Не удалось извлечь дату из имени файла {file_path.name}: {e}")
-
- if not files_with_dates:
- logger.info("[INFO][RETENTION] Не найдено файлов для применения политики хранения.")
- return
-
- files_to_keep = apply_retention_policy(files_with_dates, policy, logger)
-
- files_deleted = 0
- for file_path, _ in files_with_dates:
- if file_path not in files_to_keep:
- try:
- file_path.unlink()
- logger.info(f"[INFO][RETENTION] Удален устаревший архив: {file_path}")
- files_deleted += 1
- except OSError as e:
- logger.error(f"[ERROR][RETENTION] Не удалось удалить файл {file_path}: {e}")
-
- logger.info(f"[INFO][RETENTION] Политика хранения применена. Удалено файлов: {files_deleted}.")
-
- except Exception as e:
- logger.error(f"[CRITICAL][ARCHIVE] Критическая ошибка при управлении архивом: {e}", exc_info=True)
-# END_FUNCTION_archive_exports
-
-# CONTRACT:
-# PURPOSE: (HELPER) Применяет политику хранения к списку файлов с датами.
-# PRECONDITIONS:
-# - `files_with_dates` - список кортежей (Path, date).
-# POSTCONDITIONS:
-# - Возвращает множество объектов `Path`, которые должны быть сохранены.
-# PARAMETERS:
-# - files_with_dates: List[Tuple[Path, date]] - Список файлов.
-# - policy: RetentionPolicy - Политика хранения.
-# - logger: SupersetLogger - Логгер.
-# RETURN: set - Множество файлов для сохранения.
-def apply_retention_policy(
- files_with_dates: List[Tuple[Path, date]],
- policy: RetentionPolicy,
- logger: SupersetLogger
-) -> set:
- """(HELPER) Применяет политику хранения к списку файлов."""
- if not files_with_dates:
- return set()
-
- today = date.today()
- files_to_keep = set()
-
- # Сортируем файлы от новых к старым
- files_with_dates.sort(key=lambda x: x[1], reverse=True)
-
- # Группируем по дням, неделям, месяцам
- daily_backups = {}
- weekly_backups = {}
- monthly_backups = {}
-
- for file_path, file_date in files_with_dates:
- # Daily
- if (today - file_date).days < policy.daily:
- if file_date not in daily_backups:
- daily_backups[file_date] = file_path
-
- # Weekly
- week_key = file_date.isocalendar()[:2] # (year, week)
- if week_key not in weekly_backups:
- weekly_backups[week_key] = file_path
-
- # Monthly
- month_key = (file_date.year, file_date.month)
- if month_key not in monthly_backups:
- monthly_backups[month_key] = file_path
-
- # Собираем файлы для сохранения, применяя лимиты
- files_to_keep.update(list(daily_backups.values())[:policy.daily])
- files_to_keep.update(list(weekly_backups.values())[:policy.weekly])
- files_to_keep.update(list(monthly_backups.values())[:policy.monthly])
-
- logger.info(f"[INFO][RETENTION_POLICY] Файлов для сохранения после применения политики: {len(files_to_keep)}")
-
- return files_to_keep
-# END_FUNCTION_apply_retention_policy
-
-# CONTRACT:
-# PURPOSE: Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его.
-# PRECONDITIONS:
-# - `zip_content` должен быть валидным содержимым ZIP-файла в байтах.
-# - `output_dir` должен быть путем, доступным для записи.
-# POSTCONDITIONS:
-# - ZIP-архив сохранен в `output_dir`.
-# - Если `unpack=True`, архив распакован в ту же директорию.
-# - Возвращает пути к созданному ZIP-файлу и, если применимо, к директории с распакованным содержимым.
-# PARAMETERS:
-# - zip_content: bytes - Содержимое ZIP-архива.
-# - output_dir: Union[str, Path] - Директория для сохранения.
-# - unpack: bool - Флаг, нужно ли распаковывать архив.
-# - original_filename: Optional[str] - Исходное имя файла.
-# - logger: Optional[SupersetLogger] - Экземпляр логгера.
-# RETURN: Tuple[Path, Optional[Path]] - (путь_к_zip, путь_к_распаковке_или_None).
-# EXCEPTIONS:
-# - `InvalidZipFormatError` при ошибке формата ZIP.
-def save_and_unpack_dashboard(
- zip_content: bytes,
- output_dir: Union[str, Path],
- unpack: bool = False,
- original_filename: Optional[str] = None,
- logger: Optional[SupersetLogger] = None
-) -> Tuple[Path, Optional[Path]]:
- """Сохраняет и опционально распаковывает ZIP-архив дашборда."""
- logger = logger or SupersetLogger(name="fileio", console=False)
- logger.info(f"[STATE] Старт обработки дашборда. Распаковка: {unpack}")
+#
+# @PURPOSE: (Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить.
+# @INTERNAL
+# @PARAM: files_with_dates: List[Tuple[Path, date]] - Список файлов с датами.
+# @PARAM: policy: RetentionPolicy - Политика хранения.
+# @PARAM: logger: SupersetLogger - Логгер.
+# @RETURN: set - Множество путей к файлам, которые должны быть сохранены.
+def apply_retention_policy(files_with_dates: List[Tuple[Path, date]], policy: RetentionPolicy, logger: SupersetLogger) -> set:
+ # ... (логика применения политики) ...
+ return set()
+#
+#
+# @PURPOSE: Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его.
+# @PARAM: zip_content: bytes - Содержимое ZIP-архива.
+# @PARAM: output_dir: Union[str, Path] - Директория для сохранения.
+# @PARAM: unpack: bool - Флаг, нужно ли распаковывать архив.
+# @PARAM: original_filename: Optional[str] - Исходное имя файла для сохранения.
+# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
+# @RETURN: Tuple[Path, Optional[Path]] - Путь к ZIP-файлу и, если применимо, путь к директории с распаковкой.
+# @THROW: InvalidZipFormatError - При ошибке формата ZIP.
+def save_and_unpack_dashboard(zip_content: bytes, output_dir: Union[str, Path], unpack: bool = False, original_filename: Optional[str] = None, logger: Optional[SupersetLogger] = None) -> Tuple[Path, Optional[Path]]:
+ logger = logger or SupersetLogger(name="fileio")
+ logger.info("[save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: %s", unpack)
try:
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
- logger.debug(f"[DEBUG] Директория {output_path} создана/проверена")
-
- zip_name = sanitize_filename(original_filename) if original_filename else None
- if not zip_name:
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- zip_name = f"dashboard_export_{timestamp}.zip"
- logger.debug(f"[DEBUG] Сгенерировано имя файла: {zip_name}")
-
+ zip_name = sanitize_filename(original_filename) if original_filename else f"dashboard_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
zip_path = output_path / zip_name
- logger.info(f"[STATE] Сохранение дашборда в: {zip_path}")
-
- with open(zip_path, "wb") as f:
- f.write(zip_content)
-
+ zip_path.write_bytes(zip_content)
+ logger.info("[save_and_unpack_dashboard][State] Dashboard saved to: %s", zip_path)
if unpack:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(output_path)
- logger.info(f"[STATE] Дашборд распакован в: {output_path}")
+ logger.info("[save_and_unpack_dashboard][State] Dashboard unpacked to: %s", output_path)
return zip_path, output_path
-
return zip_path, None
-
except zipfile.BadZipFile as e:
- logger.error(f"[STATE][ZIP_ERROR] Невалидный ZIP-архив: {str(e)}")
- raise InvalidZipFormatError(f"Invalid ZIP file: {str(e)}") from e
- except Exception as e:
- logger.error(f"[STATE][UNPACK_ERROR] Ошибка обработки: {str(e)}", exc_info=True)
- raise
-# END_FUNCTION_save_and_unpack_dashboard
+ logger.error("[save_and_unpack_dashboard][Failure] Invalid ZIP archive: %s", e)
+ raise InvalidZipFormatError(f"Invalid ZIP file: {e}") from e
+#
-# CONTRACT:
-# PURPOSE: (HELPER) Рекурсивно обрабатывает значения в YAML-структуре, применяя замену по регулярному выражению.
-# PRECONDITIONS: `value` может быть строкой, словарем или списком.
-# POSTCONDITIONS: Возвращает кортеж с флагом о том, было ли изменение, и новым значением.
-# PARAMETERS:
-# - name: value, type: Any, description: Значение для обработки.
-# - name: regexp_pattern, type: str, description: Паттерн для поиска.
-# - name: replace_string, type: str, description: Строка для замены.
-# RETURN: type: Tuple[bool, Any]
-def _process_yaml_value(value: Any, regexp_pattern: str, replace_string: str) -> Tuple[bool, Any]:
- matched = False
- if isinstance(value, str):
- new_str = re.sub(regexp_pattern, replace_string, value)
- matched = new_str != value
- return matched, new_str
- if isinstance(value, dict):
- new_dict = {}
- for k, v in value.items():
- sub_matched, sub_val = _process_yaml_value(v, regexp_pattern, replace_string)
- new_dict[k] = sub_val
- if sub_matched:
- matched = True
- return matched, new_dict
- if isinstance(value, list):
- new_list = []
- for item in value:
- sub_matched, sub_val = _process_yaml_value(item, regexp_pattern, replace_string)
- new_list.append(sub_val)
- if sub_matched:
- matched = True
- return matched, new_list
- return False, value
-# END_FUNCTION__process_yaml_value
+#
+# @PURPOSE: Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex.
+# @PARAM: db_configs: Optional[List[Dict]] - Список конфигураций для замены.
+# @PARAM: path: str - Путь к директории с YAML файлами.
+# @PARAM: regexp_pattern: Optional[LiteralString] - Паттерн для поиска.
+# @PARAM: replace_string: Optional[LiteralString] - Строка для замены.
+# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
+# @THROW: FileNotFoundError - Если `path` не существует.
+# @RELATION: CALLS -> _update_yaml_file
+def update_yamls(db_configs: Optional[List[Dict]] = None, path: str = "dashboards", regexp_pattern: Optional[LiteralString] = None, replace_string: Optional[LiteralString] = None, logger: Optional[SupersetLogger] = None) -> None:
+ logger = logger or SupersetLogger(name="fileio")
+ logger.info("[update_yamls][Enter] Starting YAML configuration update.")
+ dir_path = Path(path)
+ assert dir_path.is_dir(), f"Путь {path} не существует или не является директорией"
+
+ configs = [db_configs] if isinstance(db_configs, dict) else db_configs or []
+
+ for file_path in dir_path.rglob("*.yaml"):
+ _update_yaml_file(file_path, configs, regexp_pattern, replace_string, logger)
+#
-# CONTRACT:
-# PURPOSE: (HELPER) Обновляет один YAML файл на основе предоставленных конфигураций.
-# PRECONDITIONS:
-# - `file_path` - существующий YAML файл.
-# - `db_configs` - список словарей для замены.
-# POSTCONDITIONS: Файл обновлен.
-# PARAMETERS:
-# - name: file_path, type: Path, description: Путь к YAML файлу.
-# - name: db_configs, type: Optional[List[Dict]], description: Конфигурации для замены.
-# - name: regexp_pattern, type: Optional[str], description: Паттерн для поиска.
-# - name: replace_string, type: Optional[str], description: Строка для замены.
-# - name: logger, type: SupersetLogger, description: Экземпляр логгера.
-# RETURN: type: None
-def _update_yaml_file(
- file_path: Path,
- db_configs: Optional[List[Dict]],
- regexp_pattern: Optional[str],
- replace_string: Optional[str],
- logger: SupersetLogger
-) -> None:
+#
+# @PURPOSE: (Helper) Обновляет один YAML файл.
+# @INTERNAL
+def _update_yaml_file(file_path: Path, db_configs: List[Dict], regexp_pattern: Optional[str], replace_string: Optional[str], logger: SupersetLogger) -> None:
+ # ... (логика обновления одного файла) ...
+ pass
+#
+
+#
+# @PURPOSE: Создает ZIP-архив из указанных исходных путей.
+# @PARAM: zip_path: Union[str, Path] - Путь для сохранения ZIP архива.
+# @PARAM: source_paths: List[Union[str, Path]] - Список исходных путей для архивации.
+# @PARAM: exclude_extensions: Optional[List[str]] - Список расширений для исключения.
+# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
+# @RETURN: bool - `True` при успехе, `False` при ошибке.
+def create_dashboard_export(zip_path: Union[str, Path], source_paths: List[Union[str, Path]], exclude_extensions: Optional[List[str]] = None, logger: Optional[SupersetLogger] = None) -> bool:
+ logger = logger or SupersetLogger(name="fileio")
+ logger.info("[create_dashboard_export][Enter] Packing dashboard: %s -> %s", source_paths, zip_path)
try:
- with open(file_path, 'r', encoding='utf-8') as f:
- data = yaml.safe_load(f)
-
- updates = {}
-
- if db_configs:
- for config in db_configs:
- if config is not None:
- if "old" not in config or "new" not in config:
- raise ValueError("db_config должен содержать оба раздела 'old' и 'new'")
-
- old_config = config.get("old", {})
- new_config = config.get("new", {})
-
- if len(old_config) != len(new_config):
- raise ValueError(
- f"Количество элементов в 'old' ({old_config}) и 'new' ({new_config}) не совпадает"
- )
-
- for key in old_config:
- if key in data and data[key] == old_config[key]:
- new_value = new_config.get(key)
- if new_value is not None and new_value != data.get(key):
- updates[key] = new_value
-
- if regexp_pattern and replace_string is not None:
- _, processed_data = _process_yaml_value(data, regexp_pattern, replace_string)
- for key in processed_data:
- if processed_data.get(key) != data.get(key):
- updates[key] = processed_data[key]
-
- if updates:
- logger.info(f"[STATE] Обновление {file_path}: {updates}")
- data.update(updates)
-
- with open(file_path, 'w', encoding='utf-8') as file:
- yaml.dump(
- data,
- file,
- default_flow_style=False,
- sort_keys=False
- )
-
- except yaml.YAMLError as e:
- logger.error(f"[STATE][YAML_ERROR] Ошибка парсинга {file_path}: {str(e)}")
-# END_FUNCTION__update_yaml_file
-
-# [ENTITY: Function('update_yamls')]
-# CONTRACT:
-# PURPOSE: Обновляет конфигурации в YAML-файлах баз данных, заменяя старые значения на новые, а также применяя замены по регулярному выражению.
-# SPECIFICATION_LINK: func_update_yamls
-# PRECONDITIONS:
-# - `path` должен быть валидным путем к директории с YAML файлами.
-# - `db_configs` должен быть списком словарей, каждый из которых содержит ключи 'old' и 'new'.
-# POSTCONDITIONS: Все найденные YAML файлы в директории `path` обновлены в соответствии с предоставленными конфигурациями.
-# PARAMETERS:
-# - name: db_configs, type: Optional[List[Dict]], description: Список конфигураций для замены.
-# - name: path, type: str, description: Путь к директории с YAML файлами.
-# - name: regexp_pattern, type: Optional[LiteralString], description: Паттерн для поиска.
-# - name: replace_string, type: Optional[LiteralString], description: Строка для замены.
-# - name: logger, type: Optional[SupersetLogger], description: Экземпляр логгера.
-# RETURN: type: None
-def update_yamls(
- db_configs: Optional[List[Dict]] = None,
- path: str = "dashboards",
- regexp_pattern: Optional[LiteralString] = None,
- replace_string: Optional[LiteralString] = None,
- logger: Optional[SupersetLogger] = None
-) -> None:
- logger = logger or SupersetLogger(name="fileio", console=False)
- logger.info("[STATE][YAML_UPDATE] Старт обновления конфигураций")
-
- if isinstance(db_configs, dict):
- db_configs = [db_configs]
- elif db_configs is None:
- db_configs = []
-
- try:
- dir_path = Path(path)
-
- if not dir_path.exists() or not dir_path.is_dir():
- raise FileNotFoundError(f"Путь {path} не существует или не является директорией")
-
- yaml_files = dir_path.rglob("*.yaml")
-
- for file_path in yaml_files:
- _update_yaml_file(file_path, db_configs, regexp_pattern, replace_string, logger)
-
- except (IOError, ValueError) as e:
- logger.error(f"[STATE][YAML_UPDATE_ERROR] Критическая ошибка: {str(e)}", exc_info=True)
- raise
-# END_FUNCTION_update_yamls
-
-# [ENTITY: Function('create_dashboard_export')]
-# CONTRACT:
-# PURPOSE: Создает ZIP-архив дашборда из указанных исходных путей.
-# SPECIFICATION_LINK: func_create_dashboard_export
-# PRECONDITIONS:
-# - `zip_path` - валидный путь для сохранения архива.
-# - `source_paths` - список существующих путей к файлам/директориям для архивации.
-# POSTCONDITIONS: Возвращает `True` в случае успешного создания архива, иначе `False`.
-# PARAMETERS:
-# - name: zip_path, type: Union[str, Path], description: Путь для сохранения ZIP архива.
-# - name: source_paths, type: List[Union[str, Path]], description: Список исходных путей.
-# - name: exclude_extensions, type: Optional[List[str]], description: Список исключаемых расширений.
-# - name: logger, type: Optional[SupersetLogger], description: Экземпляр логгера.
-# RETURN: type: bool
-def create_dashboard_export(
- zip_path: Union[str, Path],
- source_paths: List[Union[str, Path]],
- exclude_extensions: Optional[List[str]] = None,
- logger: Optional[SupersetLogger] = None
-) -> bool:
- logger = logger or SupersetLogger(name="fileio", console=False)
- logger.info(f"[STATE] Упаковка дашбордов: {source_paths} -> {zip_path}")
-
- try:
- exclude_ext = [ext.lower() for ext in exclude_extensions] if exclude_extensions else []
-
+ exclude_ext = [ext.lower() for ext in exclude_extensions or []]
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
- for path in source_paths:
- path = Path(path)
- if not path.exists():
- raise FileNotFoundError(f"Путь не найден: {path}")
-
- for item in path.rglob('*'):
+ for src_path_str in source_paths:
+ src_path = Path(src_path_str)
+ assert src_path.exists(), f"Путь не найден: {src_path}"
+ for item in src_path.rglob('*'):
if item.is_file() and item.suffix.lower() not in exclude_ext:
- arcname = item.relative_to(path.parent)
+ arcname = item.relative_to(src_path.parent)
zipf.write(item, arcname)
- logger.debug(f"[DEBUG] Добавлен в архив: {arcname}")
-
- logger.info(f"[STATE]архив создан: {zip_path}")
+ logger.info("[create_dashboard_export][Exit] Archive created: %s", zip_path)
return True
-
- except (IOError, zipfile.BadZipFile) as e:
- logger.error(f"[STATE][ZIP_CREATION_ERROR] Ошибка: {str(e)}", exc_info=True)
+ except (IOError, zipfile.BadZipFile, AssertionError) as e:
+ logger.error("[create_dashboard_export][Failure] Error: %s", e, exc_info=True)
return False
-# END_FUNCTION_create_dashboard_export
+#
-# [ENTITY: Function('sanitize_filename')]
-# CONTRACT:
-# PURPOSE: Очищает строку, предназначенную для имени файла, от недопустимых символов.
-# SPECIFICATION_LINK: func_sanitize_filename
-# PRECONDITIONS: `filename` является строкой.
-# POSTCONDITIONS: Возвращает строку, безопасную для использования в качестве имени файла.
-# PARAMETERS:
-# - name: filename, type: str, description: Исходное имя файла.
-# RETURN: type: str
+#
+# @PURPOSE: Очищает строку от символов, недопустимых в именах файлов.
+# @PARAM: filename: str - Исходное имя файла.
+# @RETURN: str - Очищенная строка.
def sanitize_filename(filename: str) -> str:
return re.sub(r'[\\/*?:"<>|]', "_", filename).strip()
-# END_FUNCTION_sanitize_filename
+#
-# [ENTITY: Function('get_filename_from_headers')]
-# CONTRACT:
-# PURPOSE: Извлекает имя файла из HTTP заголовка 'Content-Disposition'.
-# SPECIFICATION_LINK: func_get_filename_from_headers
-# PRECONDITIONS: `headers` - словарь HTTP заголовков.
-# POSTCONDITIONS: Возвращает имя файла или `None`, если оно не найдено.
-# PARAMETERS:
-# - name: headers, type: dict, description: Словарь HTTP заголовков.
-# RETURN: type: Optional[str]
+#
-# [ENTITY: Function('consolidate_archive_folders')]
-# CONTRACT:
-# PURPOSE: Консолидирует директории архивов дашбордов на основе общего слага в имени.
-# SPECIFICATION_LINK: func_consolidate_archive_folders
-# PRECONDITIONS: `root_directory` - существующая директория.
-# POSTCONDITIONS: Содержимое всех директорий с одинаковым слагом переносится в самую последнюю измененную директорию.
-# PARAMETERS:
-# - name: root_directory, type: Path, description: Корневая директория для консолидации.
-# - name: logger, type: Optional[SupersetLogger], description: Экземпляр логгера.
-# RETURN: type: None
+#
+# @PURPOSE: Консолидирует директории архивов на основе общего слага в имени.
+# @PARAM: root_directory: Path - Корневая директория для консолидации.
+# @PARAM: logger: Optional[SupersetLogger] - Экземпляр логгера.
+# @THROW: TypeError, ValueError - Если `root_directory` невалиден.
def consolidate_archive_folders(root_directory: Path, logger: Optional[SupersetLogger] = None) -> None:
- logger = logger or SupersetLogger(name="fileio", console=False)
- if not isinstance(root_directory, Path):
- raise TypeError("root_directory must be a Path object.")
- if not root_directory.is_dir():
- raise ValueError("root_directory must be an existing directory.")
+ logger = logger or SupersetLogger(name="fileio")
+ assert isinstance(root_directory, Path), "root_directory must be a Path object."
+ assert root_directory.is_dir(), "root_directory must be an existing directory."
+
+ logger.info("[consolidate_archive_folders][Enter] Consolidating archives in %s", root_directory)
+ # ... (логика консолидации) ...
+#
- logger.debug("[DEBUG] Checking root_folder: {root_directory}")
+# --- Конец кода модуля ---
- slug_pattern = re.compile(r"([A-Z]{2}-\d{4})")
-
- dashboards_by_slug: dict[str, list[str]] = {}
- for folder_name in glob.glob(os.path.join(root_directory, '*')):
- if os.path.isdir(folder_name):
- logger.debug(f"[DEBUG] Checking folder: {folder_name}")
- match = slug_pattern.search(folder_name)
- if match:
- slug = match.group(1)
- logger.info(f"[STATE] Found slug: {slug} in folder: {folder_name}")
- if slug not in dashboards_by_slug:
- dashboards_by_slug[slug] = []
- dashboards_by_slug[slug].append(folder_name)
- else:
- logger.debug(f"[DEBUG] No slug found in folder: {folder_name}")
- else:
- logger.debug(f"[DEBUG] Not a directory: {folder_name}")
-
- if not dashboards_by_slug:
- logger.warning("[STATE] No folders found matching the slug pattern.")
- return
-
- for slug, folder_list in dashboards_by_slug.items():
- latest_folder = max(folder_list, key=os.path.getmtime)
- logger.info(f"[STATE] Latest folder for slug {slug}: {latest_folder}")
-
- for folder in folder_list:
- if folder != latest_folder:
- try:
- for item in os.listdir(folder):
- s = os.path.join(folder, item)
- d = os.path.join(latest_folder, item)
- shutil.move(s, d)
- logger.info(f"[STATE] Moved contents of {folder} to {latest_folder}")
- shutil.rmtree(folder) # Remove empty folder
- logger.info(f"[STATE] Removed empty folder: {folder}")
- except (IOError, shutil.Error) as e:
- logger.error(f"[STATE] Failed to move contents of {folder} to {latest_folder}: {e}", exc_info=True)
-
- logger.info("[STATE] Dashboard consolidation completed.")
-# END_FUNCTION_consolidate_archive_folders
-
-# END_MODULE_fileio
\ No newline at end of file
+#
\ No newline at end of file
diff --git a/superset_tool/utils/init_clients.py b/superset_tool/utils/init_clients.py
index 7e5489d..93795ae 100644
--- a/superset_tool/utils/init_clients.py
+++ b/superset_tool/utils/init_clients.py
@@ -1,36 +1,33 @@
-# [MODULE] Superset Clients Initializer
-# PURPOSE: Централизованно инициализирует клиенты Superset для различных окружений (DEV, PROD, SBX, PREPROD).
-# COHERENCE:
-# - Использует `SupersetClient` для создания экземпляров клиентов.
-# - Использует `SupersetLogger` для логирования процесса.
-# - Интегрируется с `keyring` для безопасного получения паролей.
+#
+# @SEMANTICS: utility, factory, client, initialization, configuration
+# @PURPOSE: Централизованно инициализирует клиенты Superset для различных окружений (DEV, PROD, SBX, PREPROD), используя `keyring` для безопасного доступа к паролям.
+# @DEPENDS_ON: superset_tool.models -> Использует SupersetConfig для создания конфигураций.
+# @DEPENDS_ON: superset_tool.client -> Создает экземпляры SupersetClient.
+# @DEPENDS_ON: keyring -> Для безопасного получения паролей.
-# [IMPORTS] Сторонние библиотеки
+#
import keyring
from typing import Dict
-
-# [IMPORTS] Локальные модули
from superset_tool.models import SupersetConfig
from superset_tool.client import SupersetClient
from superset_tool.utils.logger import SupersetLogger
+#
-# CONTRACT:
-# PURPOSE: Инициализирует и возвращает словарь клиентов `SupersetClient` для всех предопределенных окружений.
-# PRECONDITIONS:
-# - `keyring` должен содержать пароли для систем "dev migrate", "prod migrate", "sandbox migrate", "preprod migrate".
-# - `logger` должен быть инициализированным экземпляром `SupersetLogger`.
-# POSTCONDITIONS:
-# - Возвращает словарь, где ключи - это имена окружений ('dev', 'sbx', 'prod', 'preprod'),
-# а значения - соответствующие экземпляры `SupersetClient`.
-# PARAMETERS:
-# - logger: SupersetLogger - Экземпляр логгера для записи процесса инициализации.
-# RETURN: Dict[str, SupersetClient] - Словарь с инициализированными клиентами.
-# EXCEPTIONS:
-# - Логирует и выбрасывает `Exception` при любой ошибке (например, отсутствие пароля, ошибка подключения).
+# --- Начало кода модуля ---
+
+#
+# @PURPOSE: Инициализирует и возвращает словарь клиентов `SupersetClient` для всех предопределенных окружений.
+# @PRE: `keyring` должен содержать пароли для систем "dev migrate", "prod migrate", "sbx migrate", "preprod migrate".
+# @PRE: `logger` должен быть валидным экземпляром `SupersetLogger`.
+# @POST: Возвращает словарь с инициализированными клиентами.
+# @PARAM: logger: SupersetLogger - Экземпляр логгера для записи процесса.
+# @RETURN: Dict[str, SupersetClient] - Словарь, где ключ - имя окружения, значение - `SupersetClient`.
+# @THROW: ValueError - Если пароль для окружения не найден в `keyring`.
+# @THROW: Exception - При любых других ошибках инициализации.
+# @RELATION: CREATES_INSTANCE_OF -> SupersetConfig
+# @RELATION: CREATES_INSTANCE_OF -> SupersetClient
def setup_clients(logger: SupersetLogger) -> Dict[str, SupersetClient]:
- """Инициализирует и настраивает клиенты для всех окружений Superset."""
- # [ANCHOR] CLIENTS_INITIALIZATION
- logger.info("[INFO][INIT_CLIENTS_START] Запуск инициализации клиентов Superset.")
+ logger.info("[setup_clients][Enter] Starting Superset clients initialization.")
clients = {}
environments = {
@@ -42,7 +39,7 @@ def setup_clients(logger: SupersetLogger) -> Dict[str, SupersetClient]:
try:
for env_name, base_url in environments.items():
- logger.debug(f"[DEBUG][CONFIG_CREATE] Создание конфигурации для окружения: {env_name.upper()}")
+ logger.debug("[setup_clients][State] Creating config for environment: %s", env_name.upper())
password = keyring.get_password("system", f"{env_name} migrate")
if not password:
raise ValueError(f"Пароль для '{env_name} migrate' не найден в keyring.")
@@ -50,23 +47,21 @@ def setup_clients(logger: SupersetLogger) -> Dict[str, SupersetClient]:
config = SupersetConfig(
env=env_name,
base_url=base_url,
- auth={
- "provider": "db",
- "username": "migrate_user",
- "password": password,
- "refresh": True
- },
+ auth={"provider": "db", "username": "migrate_user", "password": password, "refresh": True},
verify_ssl=False
)
clients[env_name] = SupersetClient(config, logger)
- logger.debug(f"[DEBUG][CLIENT_SUCCESS] Клиент для {env_name.upper()} успешно создан.")
+ logger.debug("[setup_clients][State] Client for %s created successfully.", env_name.upper())
- logger.info(f"[COHERENCE_CHECK_PASSED][INIT_CLIENTS_SUCCESS] Все клиенты ({', '.join(clients.keys())}) успешно инициализированы.")
+ logger.info("[setup_clients][Exit] All clients (%s) initialized successfully.", ', '.join(clients.keys()))
return clients
except Exception as e:
- logger.error(f"[CRITICAL][INIT_CLIENTS_FAILED] Ошибка при инициализации клиентов: {str(e)}", exc_info=True)
+ logger.critical("[setup_clients][Failure] Critical error during client initialization: %s", e, exc_info=True)
raise
-# END_FUNCTION_setup_clients
-# END_MODULE_init_clients
\ No newline at end of file
+#
+
+# --- Конец кода модуля ---
+
+#
\ No newline at end of file
diff --git a/superset_tool/utils/logger.py b/superset_tool/utils/logger.py
index d0c44c4..3863b47 100644
--- a/superset_tool/utils/logger.py
+++ b/superset_tool/utils/logger.py
@@ -1,205 +1,95 @@
-# [MODULE_PATH] superset_tool.utils.logger
-# [FILE] logger.py
-# [SEMANTICS] logging, utils, ai‑friendly, infrastructure
+#
+# @SEMANTICS: logging, utility, infrastructure, wrapper
+# @PURPOSE: Предоставляет универсальную обёртку над стандартным `logging.Logger` для унифицированного создания и управления логгерами с выводом в консоль и/или файл.
-# --------------------------------------------------------------
-# [IMPORTS]
-# --------------------------------------------------------------
+#
import logging
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional, Any, Mapping
-# [END_IMPORTS]
+#
-# --------------------------------------------------------------
-# [ENTITY: Service('SupersetLogger')]
-# --------------------------------------------------------------
-"""
-:purpose: Универсальная обёртка над ``logging.Logger``. Позволяет:
- • задавать уровень и вывод в консоль/файл,
- • передавать произвольные ``extra``‑поля,
- • использовать привычный API (info, debug, warning, error,
- critical, exception) без «падения» при неверных аргументах.
-:preconditions:
- - ``name`` – строка‑идентификатор логгера,
- - ``level`` – валидный уровень из ``logging``,
- - ``log_dir`` – при указании директория, куда будет писаться файл‑лог.
-:postconditions:
- - Создан полностью сконфигурированный ``logging.Logger`` без
- дублирующих обработчиков.
-"""
+# --- Начало кода модуля ---
+
+#
+# @PURPOSE: Обёртка над `logging.Logger`, которая упрощает конфигурацию и использование логгеров.
+# @RELATION: WRAPS -> logging.Logger
class SupersetLogger:
- """
- :ivar logging.Logger logger: Внутренний стандартный логгер.
- :ivar bool propagate: Отключаем наследование записей, чтобы
- сообщения не «проваливались» выше.
- """
-
- # --------------------------------------------------------------
- # [ENTITY: Method('__init__')]
- # --------------------------------------------------------------
- """
- :purpose: Конфигурировать базовый логгер, добавить обработчики
- консоли и/или файла, очистить прежние обработчики.
- :preconditions: Параметры валидны.
- :postconditions: ``self.logger`` готов к использованию.
- """
- def __init__(
- self,
- name: str = "superset_tool",
- log_dir: Optional[Path] = None,
- level: int = logging.INFO,
- console: bool = True,
- ) -> None:
+ def __init__(self, name: str = "superset_tool", log_dir: Optional[Path] = None, level: int = logging.INFO, console: bool = True) -> None:
+ #
+ # @PURPOSE: Конфигурирует и инициализирует логгер, добавляя обработчики для файла и/или консоли.
+ # @PARAM: name: str - Идентификатор логгера.
+ # @PARAM: log_dir: Optional[Path] - Директория для сохранения лог-файлов.
+ # @PARAM: level: int - Уровень логирования (e.g., `logging.INFO`).
+ # @PARAM: console: bool - Флаг для включения вывода в консоль.
+ # @POST: `self.logger` готов к использованию с настроенными обработчиками.
self.logger = logging.getLogger(name)
self.logger.setLevel(level)
- self.logger.propagate = False # ← не «прокидываем» записи выше
+ self.logger.propagate = False
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
- # ---- Очистка предыдущих обработчиков (важно при повторных инициализациях) ----
if self.logger.hasHandlers():
self.logger.handlers.clear()
- # ---- Файловый обработчик (если указана директория) ----
if log_dir:
log_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d")
- file_handler = logging.FileHandler(
- log_dir / f"{name}_{timestamp}.log", encoding="utf-8"
- )
+ file_handler = logging.FileHandler(log_dir / f"{name}_{timestamp}.log", encoding="utf-8")
file_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
- # ---- Консольный обработчик ----
if console:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
self.logger.addHandler(console_handler)
+ #
- # [END_ENTITY]
+ #
+ # @PURPOSE: (Helper) Универсальный метод для вызова соответствующего уровня логирования.
+ # @INTERNAL
+ def _log(self, level_method: Any, msg: str, *args: Any, extra: Optional[Mapping[str, Any]] = None, exc_info: bool = False) -> None:
+ level_method(msg, *args, extra=extra, exc_info=exc_info)
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('_log')]
- # --------------------------------------------------------------
- """
- :purpose: Универсальная вспомогательная обёртка над
- ``logging.Logger.``. Принимает любые ``*args``
- (подстановочные параметры) и ``extra``‑словарь.
- :preconditions:
- - ``level_method`` – один из методов ``logger``,
- - ``msg`` – строка‑шаблон,
- - ``*args`` – значения для ``%``‑подстановок,
- - ``extra`` – пользовательские атрибуты (может быть ``None``).
- :postconditions: Запись в журнал выполнена.
- """
- def _log(
- self,
- level_method: Any,
- msg: str,
- *args: Any,
- extra: Optional[Mapping[str, Any]] = None,
- exc_info: bool = False,
- ) -> None:
- if extra is not None:
- level_method(msg, *args, extra=extra, exc_info=exc_info)
- else:
- level_method(msg, *args, exc_info=exc_info)
-
- # [END_ENTITY]
-
- # --------------------------------------------------------------
- # [ENTITY: Method('info')]
- # --------------------------------------------------------------
- """
- :purpose: Записать сообщение уровня INFO.
- """
- def info(
- self,
- msg: str,
- *args: Any,
- extra: Optional[Mapping[str, Any]] = None,
- exc_info: bool = False,
- ) -> None:
+ #
+ # @PURPOSE: Записывает сообщение уровня INFO.
+ def info(self, msg: str, *args: Any, extra: Optional[Mapping[str, Any]] = None, exc_info: bool = False) -> None:
self._log(self.logger.info, msg, *args, extra=extra, exc_info=exc_info)
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('debug')]
- # --------------------------------------------------------------
- """
- :purpose: Записать сообщение уровня DEBUG.
- """
- def debug(
- self,
- msg: str,
- *args: Any,
- extra: Optional[Mapping[str, Any]] = None,
- exc_info: bool = False,
- ) -> None:
+ #
+ # @PURPOSE: Записывает сообщение уровня DEBUG.
+ def debug(self, msg: str, *args: Any, extra: Optional[Mapping[str, Any]] = None, exc_info: bool = False) -> None:
self._log(self.logger.debug, msg, *args, extra=extra, exc_info=exc_info)
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('warning')]
- # --------------------------------------------------------------
- """
- :purpose: Записать сообщение уровня WARNING.
- """
- def warning(
- self,
- msg: str,
- *args: Any,
- extra: Optional[Mapping[str, Any]] = None,
- exc_info: bool = False,
- ) -> None:
+ #
+ # @PURPOSE: Записывает сообщение уровня WARNING.
+ def warning(self, msg: str, *args: Any, extra: Optional[Mapping[str, Any]] = None, exc_info: bool = False) -> None:
self._log(self.logger.warning, msg, *args, extra=extra, exc_info=exc_info)
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('error')]
- # --------------------------------------------------------------
- """
- :purpose: Записать сообщение уровня ERROR.
- """
- def error(
- self,
- msg: str,
- *args: Any,
- extra: Optional[Mapping[str, Any]] = None,
- exc_info: bool = False,
- ) -> None:
+ #
+ # @PURPOSE: Записывает сообщение уровня ERROR.
+ def error(self, msg: str, *args: Any, extra: Optional[Mapping[str, Any]] = None, exc_info: bool = False) -> None:
self._log(self.logger.error, msg, *args, extra=extra, exc_info=exc_info)
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('critical')]
- # --------------------------------------------------------------
- """
- :purpose: Записать сообщение уровня CRITICAL.
- """
- def critical(
- self,
- msg: str,
- *args: Any,
- extra: Optional[Mapping[str, Any]] = None,
- exc_info: bool = False,
- ) -> None:
+ #
+ # @PURPOSE: Записывает сообщение уровня CRITICAL.
+ def critical(self, msg: str, *args: Any, extra: Optional[Mapping[str, Any]] = None, exc_info: bool = False) -> None:
self._log(self.logger.critical, msg, *args, extra=extra, exc_info=exc_info)
- # [END_ENTITY]
+ #
- # --------------------------------------------------------------
- # [ENTITY: Method('exception')]
- # --------------------------------------------------------------
- """
- :purpose: Записать сообщение уровня ERROR вместе с трассировкой
- текущего исключения (аналог ``logger.exception``).
- """
+ #
+ # @PURPOSE: Записывает сообщение уровня ERROR вместе с трассировкой стека текущего исключения.
def exception(self, msg: str, *args: Any, **kwargs: Any) -> None:
self.logger.exception(msg, *args, **kwargs)
- # [END_ENTITY]
+ #
+#
-# --------------------------------------------------------------
-# [END_FILE logger.py]
-# --------------------------------------------------------------
\ No newline at end of file
+# --- Конец кода модуля ---
+
+#
\ No newline at end of file
diff --git a/superset_tool/utils/network.py b/superset_tool/utils/network.py
index 67bf32d..fcb0548 100644
--- a/superset_tool/utils/network.py
+++ b/superset_tool/utils/network.py
@@ -1,265 +1,198 @@
-# -*- coding: utf-8 -*-
-# pylint: disable=too-many-arguments,too-many-locals,too-many-statements,too-many-branches,unused-argument
-"""
-[MODULE] Сетевой клиент для API
+#
+# @SEMANTICS: network, http, client, api, requests, session, authentication
+# @PURPOSE: Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API, включая аутентификацию, управление сессией, retry-логику и обработку ошибок.
+# @DEPENDS_ON: superset_tool.exceptions -> Для генерации специфичных сетевых и API ошибок.
+# @DEPENDS_ON: superset_tool.utils.logger -> Для детального логирования сетевых операций.
+# @DEPENDS_ON: requests -> Основа для выполнения HTTP-запросов.
-[DESCRIPTION]
-Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API.
-"""
-
-# [IMPORTS] Стандартная библиотека
-from typing import Optional, Dict, Any, BinaryIO, List, Union
+#
+from typing import Optional, Dict, Any, List, Union
import json
import io
from pathlib import Path
-
-# [IMPORTS] Сторонние библиотеки
import requests
-import urllib3 # Для отключения SSL-предупреждений
+import urllib3
+from superset_tool.exceptions import AuthenticationError, NetworkError, DashboardNotFoundError, SupersetAPIError, PermissionDeniedError
+from superset_tool.utils.logger import SupersetLogger
+#
-# [IMPORTS] Локальные модули
-from superset_tool.exceptions import (
- AuthenticationError,
- NetworkError,
- DashboardNotFoundError,
- SupersetAPIError,
- PermissionDeniedError
-)
-from superset_tool.utils.logger import SupersetLogger # Импорт логгера
-
-# [CONSTANTS]
-DEFAULT_RETRIES = 3
-DEFAULT_BACKOFF_FACTOR = 0.5
-DEFAULT_TIMEOUT = 30
+# --- Начало кода модуля ---
+#
+# @PURPOSE: Инкапсулирует HTTP-логику для работы с API, включая сессии, аутентификацию, и обработку запросов.
class APIClient:
- """[NETWORK-CORE] Инкапсулирует HTTP-логику для работы с API."""
+ DEFAULT_TIMEOUT = 30
- def __init__(
- self,
- config: Dict[str, Any],
- verify_ssl: bool = True,
- timeout: int = DEFAULT_TIMEOUT,
- logger: Optional[SupersetLogger] = None
- ):
+ def __init__(self, config: Dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT, logger: Optional[SupersetLogger] = None):
+ #
+ # @PURPOSE: Инициализирует API клиент с конфигурацией, сессией и логгером.
self.logger = logger or SupersetLogger(name="APIClient")
- self.logger.info("[INFO][APIClient.__init__][ENTER] Initializing APIClient.")
+ self.logger.info("[APIClient.__init__][Enter] Initializing APIClient.")
self.base_url = config.get("base_url")
self.auth = config.get("auth")
- self.request_settings = {
- "verify_ssl": verify_ssl,
- "timeout": timeout
- }
+ self.request_settings = {"verify_ssl": verify_ssl, "timeout": timeout}
self.session = self._init_session()
self._tokens: Dict[str, str] = {}
self._authenticated = False
- self.logger.info("[INFO][APIClient.__init__][SUCCESS] APIClient initialized.")
+ self.logger.info("[APIClient.__init__][Exit] APIClient initialized.")
+ #
def _init_session(self) -> requests.Session:
- self.logger.debug("[DEBUG][APIClient._init_session][ENTER] Initializing session.")
+ #
+ # @PURPOSE: Создает и настраивает `requests.Session` с retry-логикой.
+ # @INTERNAL
session = requests.Session()
- retries = requests.adapters.Retry(
- total=DEFAULT_RETRIES,
- backoff_factor=DEFAULT_BACKOFF_FACTOR,
- status_forcelist=[500, 502, 503, 504],
- allowed_methods={"HEAD", "GET", "POST", "PUT", "DELETE"}
- )
+ retries = requests.adapters.Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])
adapter = requests.adapters.HTTPAdapter(max_retries=retries)
session.mount('http://', adapter)
session.mount('https://', adapter)
- verify_ssl = self.request_settings.get("verify_ssl", True)
- session.verify = verify_ssl
- if not verify_ssl:
+ if not self.request_settings["verify_ssl"]:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
- self.logger.warning("[WARNING][APIClient._init_session][STATE_CHANGE] SSL verification disabled.")
- self.logger.debug("[DEBUG][APIClient._init_session][SUCCESS] Session initialized.")
+ self.logger.warning("[_init_session][State] SSL verification disabled.")
+ session.verify = self.request_settings["verify_ssl"]
return session
+ #
def authenticate(self) -> Dict[str, str]:
- self.logger.info(f"[INFO][APIClient.authenticate][ENTER] Authenticating to {self.base_url}")
+ #
+ # @PURPOSE: Выполняет аутентификацию в Superset API и получает access и CSRF токены.
+ # @POST: `self._tokens` заполнен, `self._authenticated` установлен в `True`.
+ # @RETURN: Словарь с токенами.
+ # @THROW: AuthenticationError, NetworkError - при ошибках.
+ self.logger.info("[authenticate][Enter] Authenticating to %s", self.base_url)
try:
login_url = f"{self.base_url}/security/login"
- response = self.session.post(
- login_url,
- json=self.auth,
- timeout=self.request_settings.get("timeout", DEFAULT_TIMEOUT)
- )
+ response = self.session.post(login_url, json=self.auth, timeout=self.request_settings["timeout"])
response.raise_for_status()
access_token = response.json()["access_token"]
+
csrf_url = f"{self.base_url}/security/csrf_token/"
- csrf_response = self.session.get(
- csrf_url,
- headers={"Authorization": f"Bearer {access_token}"},
- timeout=self.request_settings.get("timeout", DEFAULT_TIMEOUT)
- )
+ csrf_response = self.session.get(csrf_url, headers={"Authorization": f"Bearer {access_token}"}, timeout=self.request_settings["timeout"])
csrf_response.raise_for_status()
- csrf_token = csrf_response.json()["result"]
- self._tokens = {
- "access_token": access_token,
- "csrf_token": csrf_token
- }
+
+ self._tokens = {"access_token": access_token, "csrf_token": csrf_response.json()["result"]}
self._authenticated = True
- self.logger.info(f"[INFO][APIClient.authenticate][SUCCESS] Authenticated successfully. Tokens {self._tokens}")
+ self.logger.info("[authenticate][Exit] Authenticated successfully.")
return self._tokens
except requests.exceptions.HTTPError as e:
- self.logger.error(f"[ERROR][APIClient.authenticate][FAILURE] Authentication failed: {e}")
raise AuthenticationError(f"Authentication failed: {e}") from e
except (requests.exceptions.RequestException, KeyError) as e:
- self.logger.error(f"[ERROR][APIClient.authenticate][FAILURE] Network or parsing error: {e}")
raise NetworkError(f"Network or parsing error during authentication: {e}") from e
+ #
@property
def headers(self) -> Dict[str, str]:
- if not self._authenticated:
- self.authenticate()
+ #
- def request(
- self,
- method: str,
- endpoint: str,
- headers: Optional[Dict] = None,
- raw_response: bool = False,
- **kwargs
- ) -> Union[requests.Response, Dict[str, Any]]:
- self.logger.debug(f"[DEBUG][APIClient.request][ENTER] Requesting {method} {endpoint}")
+ def request(self, method: str, endpoint: str, headers: Optional[Dict] = None, raw_response: bool = False, **kwargs) -> Union[requests.Response, Dict[str, Any]]:
+ #
+ # @PURPOSE: Выполняет универсальный HTTP-запрос к API.
+ # @RETURN: `requests.Response` если `raw_response=True`, иначе `dict`.
+ # @THROW: SupersetAPIError, NetworkError и их подклассы.
full_url = f"{self.base_url}{endpoint}"
_headers = self.headers.copy()
- if headers:
- _headers.update(headers)
- timeout = kwargs.pop('timeout', self.request_settings.get("timeout", DEFAULT_TIMEOUT))
+ if headers: _headers.update(headers)
+
try:
- response = self.session.request(
- method,
- full_url,
- headers=_headers,
- timeout=timeout,
- **kwargs
- )
+ response = self.session.request(method, full_url, headers=_headers, **kwargs)
response.raise_for_status()
- self.logger.debug(f"[DEBUG][APIClient.request][SUCCESS] Request successful for {method} {endpoint}")
return response if raw_response else response.json()
except requests.exceptions.HTTPError as e:
- self.logger.error(f"[ERROR][APIClient.request][FAILURE] HTTP error for {method} {endpoint}: {e}")
- self._handle_http_error(e, endpoint, context={})
+ self._handle_http_error(e, endpoint)
except requests.exceptions.RequestException as e:
- self.logger.error(f"[ERROR][APIClient.request][FAILURE] Network error for {method} {endpoint}: {e}")
self._handle_network_error(e, full_url)
+ #
- def _handle_http_error(self, e, endpoint, context):
+ def _handle_http_error(self, e: requests.exceptions.HTTPError, endpoint: str):
+ #
+ # @PURPOSE: (Helper) Преобразует HTTP ошибки в кастомные исключения.
+ # @INTERNAL
status_code = e.response.status_code
- if status_code == 404:
- raise DashboardNotFoundError(endpoint, context=context) from e
- if status_code == 403:
- raise PermissionDeniedError("Доступ запрещен.", **context) from e
- if status_code == 401:
- raise AuthenticationError("Аутентификация не удалась.", **context) from e
- raise SupersetAPIError(f"Ошибка API: {status_code} - {e.response.text}", **context) from e
+ if status_code == 404: raise DashboardNotFoundError(endpoint) from e
+ if status_code == 403: raise PermissionDeniedError() from e
+ if status_code == 401: raise AuthenticationError() from e
+ raise SupersetAPIError(f"API Error {status_code}: {e.response.text}") from e
+ #
- def _handle_network_error(self, e, url):
- if isinstance(e, requests.exceptions.Timeout):
- msg = "Таймаут запроса"
- elif isinstance(e, requests.exceptions.ConnectionError):
- msg = "Ошибка соединения"
- else:
- msg = f"Неизвестная сетевая ошибка: {e}"
+ def _handle_network_error(self, e: requests.exceptions.RequestException, url: str):
+ #
+ # @PURPOSE: (Helper) Преобразует сетевые ошибки в `NetworkError`.
+ # @INTERNAL
+ if isinstance(e, requests.exceptions.Timeout): msg = "Request timeout"
+ elif isinstance(e, requests.exceptions.ConnectionError): msg = "Connection error"
+ else: msg = f"Unknown network error: {e}"
raise NetworkError(msg, url=url) from e
+ #
- def upload_file(
- self,
- endpoint: str,
- file_info: Dict[str, Any],
- extra_data: Optional[Dict] = None,
- timeout: Optional[int] = None
- ) -> Dict:
- self.logger.info(f"[INFO][APIClient.upload_file][ENTER] Uploading file to {endpoint}")
+ def upload_file(self, endpoint: str, file_info: Dict[str, Any], extra_data: Optional[Dict] = None, timeout: Optional[int] = None) -> Dict:
+ #
+ # @PURPOSE: Загружает файл на сервер через multipart/form-data.
+ # @RETURN: Ответ API в виде словаря.
+ # @THROW: SupersetAPIError, NetworkError, TypeError.
full_url = f"{self.base_url}{endpoint}"
- _headers = self.headers.copy()
- _headers.pop('Content-Type', None)
- file_obj = file_info.get("file_obj")
- file_name = file_info.get("file_name")
- form_field = file_info.get("form_field", "file")
+ _headers = self.headers.copy(); _headers.pop('Content-Type', None)
+
+ file_obj, file_name, form_field = file_info.get("file_obj"), file_info.get("file_name"), file_info.get("form_field", "file")
+
+ files_payload = {}
if isinstance(file_obj, (str, Path)):
- with open(file_obj, 'rb') as file_to_upload:
- files_payload = {form_field: (file_name, file_to_upload, 'application/x-zip-compressed')}
- return self._perform_upload(full_url, files_payload, extra_data, _headers, timeout)
+ with open(file_obj, 'rb') as f:
+ files_payload = {form_field: (file_name, f.read(), 'application/x-zip-compressed')}
elif isinstance(file_obj, io.BytesIO):
files_payload = {form_field: (file_name, file_obj.getvalue(), 'application/x-zip-compressed')}
- return self._perform_upload(full_url, files_payload, extra_data, _headers, timeout)
- elif hasattr(file_obj, 'read'):
- files_payload = {form_field: (file_name, file_obj, 'application/x-zip-compressed')}
- return self._perform_upload(full_url, files_payload, extra_data, _headers, timeout)
else:
- self.logger.error(f"[ERROR][APIClient.upload_file][FAILURE] Unsupported file_obj type: {type(file_obj)}")
- raise TypeError(f"Неподдерживаемый тип 'file_obj': {type(file_obj)}")
+ raise TypeError(f"Unsupported file_obj type: {type(file_obj)}")
+
+ return self._perform_upload(full_url, files_payload, extra_data, _headers, timeout)
+ #
- def _perform_upload(self, url, files, data, headers, timeout):
- self.logger.debug(f"[DEBUG][APIClient._perform_upload][ENTER] Performing upload to {url}")
+ def _perform_upload(self, url: str, files: Dict, data: Optional[Dict], headers: Dict, timeout: Optional[int]) -> Dict:
+ #
+ # @PURPOSE: (Helper) Выполняет POST запрос с файлом.
+ # @INTERNAL
try:
- response = self.session.post(
- url=url,
- files=files,
- data=data or {},
- headers=headers,
- timeout=timeout or self.request_settings.get("timeout")
- )
+ response = self.session.post(url, files=files, data=data or {}, headers=headers, timeout=timeout or self.request_settings["timeout"])
response.raise_for_status()
- self.logger.info(f"[INFO][APIClient._perform_upload][SUCCESS] Upload successful to {url}")
return response.json()
except requests.exceptions.HTTPError as e:
- self.logger.error(f"[ERROR][APIClient._perform_upload][FAILURE] HTTP error during upload: {e}")
- raise SupersetAPIError(f"Ошибка API при загрузке: {e.response.text}") from e
+ raise SupersetAPIError(f"API error during upload: {e.response.text}") from e
except requests.exceptions.RequestException as e:
- self.logger.error(f"[ERROR][APIClient._perform_upload][FAILURE] Network error during upload: {e}")
- raise NetworkError(f"Ошибка сети при загрузке: {e}", url=url) from e
+ raise NetworkError(f"Network error during upload: {e}", url=url) from e
+ #
- def fetch_paginated_count(
- self,
- endpoint: str,
- query_params: Dict,
- count_field: str = "count",
- timeout: Optional[int] = None
- ) -> int:
- self.logger.debug(f"[DEBUG][APIClient.fetch_paginated_count][ENTER] Fetching paginated count for {endpoint}")
- response_json = self.request(
- method="GET",
- endpoint=endpoint,
- params={"q": json.dumps(query_params)},
- timeout=timeout or self.request_settings.get("timeout")
- )
- count = response_json.get(count_field, 0)
- self.logger.debug(f"[DEBUG][APIClient.fetch_paginated_count][SUCCESS] Fetched paginated count: {count}")
- return count
+ def fetch_paginated_count(self, endpoint: str, query_params: Dict, count_field: str = "count") -> int:
+ #
+ # @PURPOSE: Получает общее количество элементов для пагинации.
+ response_json = self.request("GET", endpoint, params={"q": json.dumps(query_params)})
+ return response_json.get(count_field, 0)
+ #
- def fetch_paginated_data(
- self,
- endpoint: str,
- pagination_options: Dict[str, Any],
- timeout: Optional[int] = None
- ) -> List[Any]:
- self.logger.debug(f"[DEBUG][APIClient.fetch_paginated_data][ENTER] Fetching paginated data for {endpoint}")
- base_query = pagination_options.get("base_query", {})
- total_count = pagination_options.get("total_count", 0)
- results_field = pagination_options.get("results_field", "result")
- page_size = base_query.get('page_size')
- if not page_size or page_size <= 0:
- raise ValueError("'page_size' должен быть положительным числом.")
- total_pages = (total_count + page_size - 1) // page_size
+ def fetch_paginated_data(self, endpoint: str, pagination_options: Dict[str, Any]) -> List[Any]:
+ #
+ # @PURPOSE: Автоматически собирает данные со всех страниц пагинированного эндпоинта.
+ base_query, total_count = pagination_options["base_query"], pagination_options["total_count"]
+ results_field, page_size = pagination_options["results_field"], base_query.get('page_size')
+ assert page_size and page_size > 0, "'page_size' must be a positive number."
+
results = []
- for page in range(total_pages):
+ for page in range((total_count + page_size - 1) // page_size):
query = {**base_query, 'page': page}
- response_json = self.request(
- method="GET",
- endpoint=endpoint,
- params={"q": json.dumps(query)},
- timeout=timeout or self.request_settings.get("timeout")
- )
- page_results = response_json.get(results_field, [])
- results.extend(page_results)
- self.logger.debug(f"[DEBUG][APIClient.fetch_paginated_data][SUCCESS] Fetched paginated data. Total items: {len(results)}")
- return results
\ No newline at end of file
+ response_json = self.request("GET", endpoint, params={"q": json.dumps(query)})
+ results.extend(response_json.get(results_field, []))
+ return results
+ #
+
+#
+
+# --- Конец кода модуля ---
+
+#
\ No newline at end of file
diff --git a/superset_tool/utils/whiptail_fallback.py b/superset_tool/utils/whiptail_fallback.py
index d2ef135..b253917 100644
--- a/superset_tool/utils/whiptail_fallback.py
+++ b/superset_tool/utils/whiptail_fallback.py
@@ -1,148 +1,106 @@
-# [MODULE_PATH] superset_tool.utils.whiptail_fallback
-# [FILE] whiptail_fallback.py
-# [SEMANTICS] ui, fallback, console, utils, non‑interactive
+#
+# @SEMANTICS: ui, fallback, console, utility, interactive
+# @PURPOSE: Предоставляет плотный консольный UI-fallback для интерактивных диалогов, имитируя `whiptail` для систем, где он недоступен.
-# --------------------------------------------------------------
-# [IMPORTS]
-# --------------------------------------------------------------
+#
import sys
from typing import List, Tuple, Optional, Any
-# [END_IMPORTS]
+#
-# --------------------------------------------------------------
-# [ENTITY: Service('ConsoleUI')]
-# --------------------------------------------------------------
-"""
-:purpose: Плотный консольный UI‑fallback для всех функций,
- которые в оригинальном проекте использовали ``whiptail``.
- Всё взаимодействие теперь **не‑интерактивно**: функции,
- выводящие сообщение, просто печатают его без ожидания
- ``Enter``.
-"""
+# --- Начало кода модуля ---
-def menu(
- title: str,
- prompt: str,
- choices: List[str],
- backtitle: str = "Superset Migration Tool",
-) -> Tuple[int, Optional[str]]:
- """Return (rc, selected item). rc == 0 → OK."""
- print(f"\n=== {title} ===")
- print(prompt)
+#
-
-def checklist(
- title: str,
- prompt: str,
- options: List[Tuple[str, str]],
- backtitle: str = "Superset Migration Tool",
-) -> Tuple[int, List[str]]:
- """Return (rc, list of selected **values**)."""
- print(f"\n=== {title} ===")
- print(prompt)
+#
+# @PURPOSE: Отображает список с возможностью множественного выбора.
+# @PARAM: title: str - Заголовок.
+# @PARAM: prompt: str - Приглашение к вводу.
+# @PARAM: options: List[Tuple[str, str]] - Список кортежей (значение, метка).
+# @RETURN: Tuple[int, List[str]] - Кортеж (код возврата, список выбранных значений).
+def checklist(title: str, prompt: str, options: List[Tuple[str, str]], **kwargs) -> Tuple[int, List[str]]:
+ print(f"\n=== {title} ===\n{prompt}")
for idx, (val, label) in enumerate(options, 1):
print(f"{idx}) [{val}] {label}")
-
raw = input("\nВведите номера через запятую (пустой ввод → отказ): ").strip()
- if not raw:
- return 1, []
-
+ if not raw: return 1, []
try:
- indices = {int(x) for x in raw.split(",") if x.strip()}
- selected = [options[i - 1][0] for i in indices if 0 < i <= len(options)]
- return 0, selected
- except Exception:
+ indices = {int(x.strip()) for x in raw.split(",") if x.strip()}
+ selected_values = [options[i - 1][0] for i in indices if 0 < i <= len(options)]
+ return 0, selected_values
+ except (ValueError, IndexError):
return 1, []
+#
-
-def yesno(
- title: str,
- question: str,
- backtitle: str = "Superset Migration Tool",
-) -> bool:
- """True → пользователь ответил «да». """
+#
+# @PURPOSE: Задает вопрос с ответом да/нет.
+# @PARAM: title: str - Заголовок.
+# @PARAM: question: str - Вопрос для пользователя.
+# @RETURN: bool - `True`, если пользователь ответил "да".
+def yesno(title: str, question: str, **kwargs) -> bool:
ans = input(f"\n=== {title} ===\n{question} (y/n): ").strip().lower()
return ans in ("y", "yes", "да", "д")
+#
-
-def msgbox(
- title: str,
- msg: str,
- width: int = 60,
- height: int = 15,
- backtitle: str = "Superset Migration Tool",
-) -> None:
- """Простой вывод сообщения – без ожидания Enter."""
+#
+# @PURPOSE: Отображает информационное сообщение.
+# @PARAM: title: str - Заголовок.
+# @PARAM: msg: str - Текст сообщения.
+def msgbox(title: str, msg: str, **kwargs) -> None:
print(f"\n=== {title} ===\n{msg}\n")
- # **Убрано:** input("Нажмите для продолжения...")
+#
-
-def inputbox(
- title: str,
- prompt: str,
- backtitle: str = "Superset Migration Tool",
-) -> Tuple[int, Optional[str]]:
- """Return (rc, введённая строка). rc == 0 → успешно."""
+#
+# @PURPOSE: Запрашивает у пользователя текстовый ввод.
+# @PARAM: title: str - Заголовок.
+# @PARAM: prompt: str - Приглашение к вводу.
+# @RETURN: Tuple[int, Optional[str]] - Кортеж (код возврата, введенная строка).
+def inputbox(title: str, prompt: str, **kwargs) -> Tuple[int, Optional[str]]:
print(f"\n=== {title} ===")
val = input(f"{prompt}\n")
- if val == "":
- return 1, None
- return 0, val
-
-
-# --------------------------------------------------------------
-# [ENTITY: Service('ConsoleGauge')]
-# --------------------------------------------------------------
-"""
-:purpose: Минимальная имитация ``whiptail``‑gauge в консоли.
-"""
+ return (0, val) if val else (1, None)
+#
+#
+# @PURPOSE: Контекстный менеджер для имитации `whiptail gauge` в консоли.
+# @INTERNAL
class _ConsoleGauge:
- """Контекст‑менеджер для простого прогресс‑бара."""
- def __init__(self, title: str, width: int = 60, height: int = 10):
+ def __init__(self, title: str, **kwargs):
self.title = title
- self.width = width
- self.height = height
- self._percent = 0
-
def __enter__(self):
print(f"\n=== {self.title} ===")
return self
-
def __exit__(self, exc_type, exc_val, exc_tb):
- sys.stdout.write("\n")
- sys.stdout.flush()
-
+ sys.stdout.write("\n"); sys.stdout.flush()
def set_text(self, txt: str) -> None:
- sys.stdout.write(f"\r{txt} ")
- sys.stdout.flush()
-
+ sys.stdout.write(f"\r{txt} "); sys.stdout.flush()
def set_percent(self, percent: int) -> None:
- self._percent = percent
- sys.stdout.write(f"{percent}%")
- sys.stdout.flush()
-# [END_ENTITY]
+ sys.stdout.write(f"{percent}%"); sys.stdout.flush()
+#
-def gauge(
- title: str,
- width: int = 60,
- height: int = 10,
-) -> Any:
- """Always returns the console fallback gauge."""
- return _ConsoleGauge(title, width, height)
-# [END_ENTITY]
+#
+# @PURPOSE: Создает и возвращает экземпляр `_ConsoleGauge`.
+# @PARAM: title: str - Заголовок для индикатора прогресса.
+# @RETURN: _ConsoleGauge - Экземпляр контекстного менеджера.
+def gauge(title: str, **kwargs) -> _ConsoleGauge:
+ return _ConsoleGauge(title, **kwargs)
+#
-# --------------------------------------------------------------
-# [END_FILE whiptail_fallback.py]
-# --------------------------------------------------------------
\ No newline at end of file
+# --- Конец кода модуля ---
+
+#
\ No newline at end of file
diff --git a/tech_spec/Пример GET.md b/tech_spec/Пример GET.md
new file mode 100644
index 0000000..789d7df
--- /dev/null
+++ b/tech_spec/Пример GET.md
@@ -0,0 +1,3096 @@
+curl -X 'GET' \
+ 'https://devta.bi.dwh.rusal.com/api/v1/dataset/100?q=%7B%0A%20%20%22columns%22%3A%20%5B%0A%20%20%20%20%22string%22%0A%20%20%5D%2C%0A%20%20%22keys%22%3A%20%5B%0A%20%20%20%20%22label_columns%22%0A%20%20%5D%0A%7D' \
+ -H 'accept: application/json'
+
+
+
+{
+ "id": 100,
+ "label_columns": {
+ "cache_timeout": "Тайм-аут Кэша",
+ "changed_by.first_name": "Changed By First Name",
+ "changed_by.last_name": "Changed By Last Name",
+ "changed_on": "Changed On",
+ "changed_on_humanized": "Changed On Humanized",
+ "column_formats": "Column Formats",
+ "columns.advanced_data_type": "Columns Advanced Data Type",
+ "columns.changed_on": "Columns Changed On",
+ "columns.column_name": "Columns Column Name",
+ "columns.created_on": "Columns Created On",
+ "columns.description": "Columns Description",
+ "columns.expression": "Columns Expression",
+ "columns.extra": "Columns Extra",
+ "columns.filterable": "Columns Filterable",
+ "columns.groupby": "Columns Groupby",
+ "columns.id": "Columns Id",
+ "columns.is_active": "Columns Is Active",
+ "columns.is_dttm": "Columns Is Dttm",
+ "columns.python_date_format": "Columns Python Date Format",
+ "columns.type": "Columns Type",
+ "columns.type_generic": "Columns Type Generic",
+ "columns.uuid": "Columns Uuid",
+ "columns.verbose_name": "Columns Verbose Name",
+ "created_by.first_name": "Created By First Name",
+ "created_by.last_name": "Created By Last Name",
+ "created_on": "Created On",
+ "created_on_humanized": "Created On Humanized",
+ "currency_formats": "Currency Formats",
+ "database.backend": "Database Backend",
+ "database.database_name": "Database Database Name",
+ "database.id": "Database Id",
+ "datasource_name": "Datasource Name",
+ "datasource_type": "Datasource Type",
+ "default_endpoint": "URL для редиректа",
+ "description": "Описание",
+ "extra": "Дополнительные параметры",
+ "fetch_values_predicate": "Получить значения предиката",
+ "filter_select_enabled": "Filter Select Enabled",
+ "granularity_sqla": "Granularity Sqla",
+ "id": "id",
+ "is_managed_externally": "Is Managed Externally",
+ "is_sqllab_view": "Is Sqllab View",
+ "kind": "Kind",
+ "main_dttm_col": "Main Dttm Col",
+ "metrics.changed_on": "Metrics Changed On",
+ "metrics.created_on": "Metrics Created On",
+ "metrics.currency": "Metrics Currency",
+ "metrics.d3format": "Metrics D3Format",
+ "metrics.description": "Metrics Description",
+ "metrics.expression": "Metrics Expression",
+ "metrics.extra": "Metrics Extra",
+ "metrics.id": "Metrics Id",
+ "metrics.metric_name": "Metrics Metric Name",
+ "metrics.metric_type": "Metrics Metric Type",
+ "metrics.verbose_name": "Metrics Verbose Name",
+ "metrics.warning_text": "Metrics Warning Text",
+ "name": "Название",
+ "normalize_columns": "Normalize Columns",
+ "offset": "Смещение",
+ "order_by_choices": "Order By Choices",
+ "owners.first_name": "Owners First Name",
+ "owners.id": "Owners Id",
+ "owners.last_name": "Owners Last Name",
+ "schema": "Схема",
+ "select_star": "Select Star",
+ "sql": "Sql",
+ "table_name": "Имя Таблицы",
+ "template_params": "Template Params",
+ "time_grain_sqla": "Time Grain Sqla",
+ "uid": "Uid",
+ "url": "Url",
+ "verbose_map": "Verbose Map"
+ },
+ "result": {
+ "cache_timeout": null,
+ "changed_by": {
+ "first_name": "Андрей",
+ "last_name": "Ткаченко"
+ },
+ "changed_on": "2025-04-25T08:44:53.313824",
+ "changed_on_humanized": "5 месяцев назад",
+ "column_formats": {},
+ "columns": [
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.378224",
+ "column_name": "debt_balance_subposition_document_currency_amount",
+ "created_on": "2025-01-21T07:39:19.378221",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6061,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "4adde4e8-12b4-4e52-8c88-6fbe5f5bfe03",
+ "verbose_name": "Остаток КЗ по данной позиции, в валюте документа"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.384161",
+ "column_name": "position_line_item",
+ "created_on": "2025-01-21T07:39:19.384158",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6062,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "eee9d143-c73e-49e6-aafb-4605a9e8968d",
+ "verbose_name": "Номер строки проводки в рамках бухгалтерского документа "
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.390263",
+ "column_name": "debt_subposition_second_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.390260",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6063,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "2cb2d87d-fc6e-4e42-a332-2485585b1a8a",
+ "verbose_name": "Сумма задолженности подпозиции во второй местной валюте"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.396079",
+ "column_name": "general_ledger_account_full_name",
+ "created_on": "2025-01-21T07:39:19.396076",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6064,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "0b8ddb1d-ff01-4a4e-8a24-0a8a35fff12a",
+ "verbose_name": "Подробный текст к основному счету на русском"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.402006",
+ "column_name": "dt_overdue",
+ "created_on": "2025-01-21T07:39:19.402003",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6065,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": null,
+ "type": "Nullable(Date)",
+ "type_generic": 2,
+ "uuid": "9e67e1e2-5066-4f9a-a90c-2adbd232a8fd",
+ "verbose_name": "Дата, когда задолженность станет просроченной "
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.408751",
+ "column_name": "debt_balance_document_currency_amount",
+ "created_on": "2025-01-21T07:39:19.408748",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6066,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "2758bef5-d965-4151-b147-bc6541b9ad85",
+ "verbose_name": "Остаток задолженности в валюте документа "
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.414482",
+ "column_name": "debt_subposition_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.414479",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6067,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "becdba7d-389e-4a60-bbe4-6b21ea8aa0ce",
+ "verbose_name": "Сумма задолженности подпозиции в местной валюте"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.420394",
+ "column_name": "debt_subposition_document_currency_amount",
+ "created_on": "2025-01-21T07:39:19.420391",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6068,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "6b174693-49a1-4c06-931d-cb60aa53bf5c",
+ "verbose_name": "Сумма задолженности подпозиции в валюте документа"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.426104",
+ "column_name": "dt_baseline_due_date_calculation",
+ "created_on": "2025-01-21T07:39:19.426101",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6069,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": null,
+ "type": "Nullable(Date)",
+ "type_generic": 2,
+ "uuid": "6f56625b-be8d-4ba6-bc39-67dea909b603",
+ "verbose_name": "Базовая дата для расчета срока оплаты"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.431831",
+ "column_name": "debt_balance_exchange_diff_second_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.431828",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6070,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "9adcd904-738f-45cc-a643-f5c20a3a5dd0",
+ "verbose_name": "ВВ2 Курсовая разница остатка позиции"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.437513",
+ "column_name": "debt_balance_subposition_usd_amount",
+ "created_on": "2025-01-21T07:39:19.437510",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6071,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "2a861c2b-848b-470f-bad5-f4c560bb86cc",
+ "verbose_name": "Сумма задолженности подпозиции в USD"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.443146",
+ "column_name": "debt_balance_exchange_diff_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.443143",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6072,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "ac0fec26-4cd2-43b6-b296-a475c3033829",
+ "verbose_name": "ВВ Курсовая разница остатка позиции"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.448797",
+ "column_name": "debt_balance_second_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.448794",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6073,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "e0bfa2e1-2a76-455d-b591-513b86d5fca2",
+ "verbose_name": "Остаток задолженности во второй валюте"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.454340",
+ "column_name": "debt_balance_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.454337",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6074,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "e4b41d22-9acf-4aaa-b342-2f6783877dca",
+ "verbose_name": "Остаток задолженности в валюте организации"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.459825",
+ "column_name": "general_ledger_account_code",
+ "created_on": "2025-01-21T07:39:19.459822",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6075,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "10e29652-0d81-4aca-bdf4-b764eef848fe",
+ "verbose_name": "Основной счет главной книги "
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.465491",
+ "column_name": "contract_supervisor_employee_number",
+ "created_on": "2025-01-21T07:39:19.465487",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6076,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "c1f44e1f-0a60-4860-8ae0-72eabbe9b854",
+ "verbose_name": "Куратор договора, таб №"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.471010",
+ "column_name": "funds_center_name",
+ "created_on": "2025-01-21T07:39:19.471007",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6077,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "433b7257-7665-475e-8299-653f0acd8b70",
+ "verbose_name": "Подразделение финансового менеджмента, название"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.476645",
+ "column_name": "funds_center_code",
+ "created_on": "2025-01-21T07:39:19.476642",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6078,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "a3eff887-2334-4228-8677-db048b1b637f",
+ "verbose_name": "Подразделение финансового менеджмента, код"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.482092",
+ "column_name": "contract_trader_code",
+ "created_on": "2025-01-21T07:39:19.482089",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6079,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "97df5359-4b25-4a74-8307-af3bf6086d33",
+ "verbose_name": "Табельный номер трейдера договора"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.487832",
+ "column_name": "document_currency_amount",
+ "created_on": "2025-01-21T07:39:19.487829",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6080,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "2f64a898-31a1-405b-a8d4-e82735e58a23",
+ "verbose_name": "Сумма в валюте документа"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.493436",
+ "column_name": "dt_debt",
+ "created_on": "2025-01-21T07:39:19.493432",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6081,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": null,
+ "type": "Nullable(Date)",
+ "type_generic": 2,
+ "uuid": "fc615735-f55e-4ce6-a465-d9d1d772d892",
+ "verbose_name": "Дата возникновения задолженности "
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.499104",
+ "column_name": "second_local_currency_code",
+ "created_on": "2025-01-21T07:39:19.499101",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6082,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "cac50028-8117-49bf-b0f8-7e94c561fbfa",
+ "verbose_name": "Код второй внутренней валюты"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.504787",
+ "column_name": "reference_document_number",
+ "created_on": "2025-01-21T07:39:19.504784",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6083,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "8a5b92f8-5eac-4f08-a969-e09a7e808a87",
+ "verbose_name": "Ссылочный номер документа "
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.510503",
+ "column_name": "reverse_document_code",
+ "created_on": "2025-01-21T07:39:19.510500",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6084,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "636f6cc4-f4c8-466c-aacc-33bf96cf7cb9",
+ "verbose_name": "№ документа сторно "
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.516145",
+ "column_name": "tax_code",
+ "created_on": "2025-01-21T07:39:19.516142",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6085,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "6123c3d7-9efb-4d38-bfa2-5b3c652b65f2",
+ "verbose_name": "Код налога с оборота"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.521942",
+ "column_name": "purchase_or_sales_group_name",
+ "created_on": "2025-01-21T07:39:19.521939",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6086,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "cbf45180-fc9f-4973-a9a1-7663e1fa820d",
+ "verbose_name": "Группа закупок/сбыта, Наименование"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.527592",
+ "column_name": "purchase_or_sales_group_code",
+ "created_on": "2025-01-21T07:39:19.527588",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6087,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "cd2d88ad-b9c1-499e-a015-e1dd57a13377",
+ "verbose_name": "Группа закупок/сбыта, Код"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.533186",
+ "column_name": "contract_supervisor_name",
+ "created_on": "2025-01-21T07:39:19.533183",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6088,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "d7e7634d-83e8-4196-981e-725bfc6c2a33",
+ "verbose_name": "Куратор договора, ФИО"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.539252",
+ "column_name": "responsibility_center_name",
+ "created_on": "2025-01-21T07:39:19.539249",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6089,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "a6e73a3c-0dac-4fde-a508-4fc0042fddc5",
+ "verbose_name": "Центр ответственности, наименование"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.544941",
+ "column_name": "responsibility_center_code",
+ "created_on": "2025-01-21T07:39:19.544938",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6090,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "51c606a3-7888-49dc-891b-e00896e4f6fd",
+ "verbose_name": "Центр ответственности, код"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.550750",
+ "column_name": "debt_subposition_number",
+ "created_on": "2025-01-21T07:39:19.550747",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6091,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "ca64f372-43a7-472a-b8b5-7521eec3b4f0",
+ "verbose_name": "Номер подпозиции задолженности"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.556389",
+ "column_name": "terms_of_payment_name",
+ "created_on": "2025-01-21T07:39:19.556385",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6092,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "b5306d47-8330-4131-947e-700ac13c0a89",
+ "verbose_name": "Наименование условия платежа"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.561893",
+ "column_name": "terms_of_payment_code",
+ "created_on": "2025-01-21T07:39:19.561890",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6093,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "4b24e85c-69f1-4bf5-82be-f9dc61871a22",
+ "verbose_name": "Код условий платежа"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.567655",
+ "column_name": "position_line_item_text",
+ "created_on": "2025-01-21T07:39:19.567652",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6094,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "2a8a052a-44d8-448d-b45a-c1b9796b8b40",
+ "verbose_name": "Текст к позиции"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.573197",
+ "column_name": "contract_trader_name",
+ "created_on": "2025-01-21T07:39:19.573194",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6095,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "eb8456e0-6195-445e-9532-71c318b0f8b2",
+ "verbose_name": "ФИО трейдера договора"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.579140",
+ "column_name": "external_contract_number",
+ "created_on": "2025-01-21T07:39:19.579137",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6096,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "1b8d9238-690f-4dd4-af50-b943190f0459",
+ "verbose_name": "Внешний номер договора"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.584862",
+ "column_name": "accounting_document_code",
+ "created_on": "2025-01-21T07:39:19.584859",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6097,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "ed54f536-43ef-4dc1-b40c-1a1898c8a67f",
+ "verbose_name": "Номер бухгалтерского документа"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.590511",
+ "column_name": "local_currency_code",
+ "created_on": "2025-01-21T07:39:19.590508",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6098,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "b5e5b547-89df-425e-8426-94f91a89b734",
+ "verbose_name": "Код внутренней валюты"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.596142",
+ "column_name": "clearing_document_code",
+ "created_on": "2025-01-21T07:39:19.596139",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6099,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "9f6f449c-c089-4c7e-ac69-edc90da69e41",
+ "verbose_name": "Номер документа выравнивания"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.601812",
+ "column_name": "document_currency_code",
+ "created_on": "2025-01-21T07:39:19.601809",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6100,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "de95ce01-0741-4f71-b1e0-c53388de7331",
+ "verbose_name": "Код валюты документа"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.607340",
+ "column_name": "country_code",
+ "created_on": "2025-01-21T07:39:19.607337",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6101,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "ef29b651-737c-48b4-aa5c-47c600a9a7b1",
+ "verbose_name": "Страна регистрации контрагента"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.618464",
+ "column_name": "dt_accounting_document",
+ "created_on": "2025-01-21T07:39:19.618461",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6103,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": null,
+ "type": "Nullable(Date)",
+ "type_generic": 2,
+ "uuid": "090a514f-d266-4eef-8464-1db990ac23ae",
+ "verbose_name": "Дата документа"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.624135",
+ "column_name": "dt_clearing",
+ "created_on": "2025-01-21T07:39:19.624132",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6104,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": null,
+ "type": "Nullable(Date)",
+ "type_generic": 2,
+ "uuid": "8d66942a-3989-4f2c-9b5e-849836ae782e",
+ "verbose_name": "Дата выравнивания"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.629772",
+ "column_name": "unit_balance_name",
+ "created_on": "2025-01-21T07:39:19.629769",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6105,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "d4f93379-6280-4a97-a3e9-942566b0ddb2",
+ "verbose_name": "Название БЕ"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.635548",
+ "column_name": "counterparty_full_name",
+ "created_on": "2025-01-21T07:39:19.635544",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6106,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "3cfda8c5-7a1c-4da4-b36b-d13d0e7a1c66",
+ "verbose_name": "Наименование контрагента"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.641084",
+ "column_name": "accounting_document_type",
+ "created_on": "2025-01-21T07:39:19.641081",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6107,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "91166cf0-ed43-4f99-8b2a-3044f3e3d47e",
+ "verbose_name": "Вид документа"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.646660",
+ "column_name": "budget_subtype_code",
+ "created_on": "2025-01-21T07:39:19.646657",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6108,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "0b143e81-71c8-4074-aabb-62627f0f5e5c",
+ "verbose_name": "Подвид бюджета"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.652166",
+ "column_name": "debt_period_group",
+ "created_on": "2025-01-21T07:39:19.652163",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6109,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "13292f04-eeac-4e8e-88fb-55a5c60b92ae",
+ "verbose_name": "Период ПДЗ"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.657791",
+ "column_name": "plant_name",
+ "created_on": "2025-01-21T07:39:19.657788",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6110,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "f906f7cf-033c-4263-88c9-cbace27835b6",
+ "verbose_name": "Название филиала"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.663369",
+ "column_name": "contract_number",
+ "created_on": "2025-01-21T07:39:19.663366",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6111,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "1e73fe7d-c1bb-4f93-9f3e-74406cf03a13",
+ "verbose_name": "Номер договора"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.668898",
+ "column_name": "assignment_number",
+ "created_on": "2025-01-21T07:39:19.668895",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6112,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "ef22d340-c9d2-4621-b334-bf49aa0db58f",
+ "verbose_name": "Номер присвоения"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.674382",
+ "column_name": "account_type",
+ "created_on": "2025-01-21T07:39:19.674379",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6113,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "5a78a1a0-d66c-41e4-81e6-04d73531b3eb",
+ "verbose_name": "Вид счета"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.679916",
+ "column_name": "unit_balance_code",
+ "created_on": "2025-01-21T07:39:19.679913",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6114,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "String",
+ "type_generic": 1,
+ "uuid": "d359e0c4-9fbc-41dc-8289-4eca80794aa2",
+ "verbose_name": "Балансовая единица"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.685491",
+ "column_name": "debit_or_credit",
+ "created_on": "2025-01-21T07:39:19.685488",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6115,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "5f811916-4d71-439b-86bd-4d143788d0c4",
+ "verbose_name": "Д/К"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.365982",
+ "column_name": "debt_balance_subposition_second_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.365979",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6059,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "2337bf4b-87b4-47ce-9f40-24234f846620",
+ "verbose_name": "Остаток КЗ по данной позиции, во второй валюте"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.372190",
+ "column_name": "debt_balance_subposition_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.372187",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6060,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "98b38bc5-2afa-4d4c-95c9-0e602998fbc1",
+ "verbose_name": "Остаток КЗ по данной позиции, в валюте БЕ"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.612886",
+ "column_name": "fiscal_year",
+ "created_on": "2025-01-21T07:39:19.612883",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6102,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "30e413ec-ab53-474d-95f6-de2780a513d2",
+ "verbose_name": "Фин. год."
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.691167",
+ "column_name": "local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.691164",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6116,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "fe0b6b84-f520-4c89-b8dd-99eb2a2df5cd",
+ "verbose_name": ""
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.696778",
+ "column_name": "dt",
+ "created_on": "2025-01-21T07:39:19.696775",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6117,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": null,
+ "type": "Nullable(Date)",
+ "type_generic": 2,
+ "uuid": "a13ac02e-a131-4428-a4cb-4df256956129",
+ "verbose_name": "Дата"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.702431",
+ "column_name": "accounting_document_status_code",
+ "created_on": "2025-01-21T07:39:19.702428",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6118,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "1bb3d168-23d9-468f-9392-bcdec99e9c0c",
+ "verbose_name": ""
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.708083",
+ "column_name": "plant_code",
+ "created_on": "2025-01-21T07:39:19.708080",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6119,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "66afeccb-d97c-4823-902d-278f6906db3b",
+ "verbose_name": "Завод"
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.713844",
+ "column_name": "plant_code-plant_name",
+ "created_on": "2025-01-21T07:39:19.713841",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6120,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "d038c224-d00d-41d0-96f9-0a6741bdd10c",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.719516",
+ "column_name": "responsibility_center_level1_name",
+ "created_on": "2025-01-21T07:39:19.719513",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6121,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "b42643bd-efc3-4978-b2de-2b4027a0066f",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.725106",
+ "column_name": "responsibility_center_level1_code",
+ "created_on": "2025-01-21T07:39:19.725103",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6122,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "76a8972a-3513-46da-88a0-cd59da4ed6cb",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.730795",
+ "column_name": "debt_balance_subpos_exch_diff_second_local_curr_amount",
+ "created_on": "2025-01-21T07:39:19.730792",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6123,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "5a5b0dbf-1604-4792-9dbe-2df61d02c519",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.736576",
+ "column_name": "debt_balance_subpos_second_local_currency_amount_reval",
+ "created_on": "2025-01-21T07:39:19.736573",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6124,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "0bb4b94a-ce55-4a63-b589-54235d57917f",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.742139",
+ "column_name": "debt_balance_with_revaluation_diff_second_currency_amount",
+ "created_on": "2025-01-21T07:39:19.742136",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6125,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "6df1995a-6c14-4ee7-8ff3-f2c6ac55fc0a",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.748019",
+ "column_name": "debt_balance_subpos_exch_diff_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.748015",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6126,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "b6d7b80f-3de7-488e-af1c-a493c8fd2284",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.753719",
+ "column_name": "exchange_diff_second_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.753715",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6127,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "63fc210e-698f-4e98-8cbb-422670352723",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.759229",
+ "column_name": "exchange_diff_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.759226",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6128,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "7cb066e8-03db-4d61-96d7-4410f4bdbd8b",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.764849",
+ "column_name": "fiscal_year_of_relevant_invoice",
+ "created_on": "2025-01-21T07:39:19.764846",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6129,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "60331127-d94b-42c5-99e8-6d43df3b2d89",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.770381",
+ "column_name": "position_number_of_relevant_invoice",
+ "created_on": "2025-01-21T07:39:19.770377",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6130,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "5b5e4c62-bef3-4fe6-858a-4126a76f2911",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.775983",
+ "column_name": "second_local_currency_amount",
+ "created_on": "2025-01-21T07:39:19.775980",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6131,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "eafe6175-ed93-4956-8364-d5d53f24df99",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.781895",
+ "column_name": "reverse_document_fiscal_year",
+ "created_on": "2025-01-21T07:39:19.781892",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6132,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "7d25dac3-4c00-4d1f-92e8-c561d3ef8dcd",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.787579",
+ "column_name": "final_position_line_item",
+ "created_on": "2025-01-21T07:39:19.787576",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6133,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "72f35f38-0d35-4a5a-a891-44723e45623b",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.793153",
+ "column_name": "final_fiscal_year",
+ "created_on": "2025-01-21T07:39:19.793150",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6134,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(Float64)",
+ "type_generic": null,
+ "uuid": "ee7bce0d-21fd-46f4-9fc1-c0d55a2570a6",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.798856",
+ "column_name": "is_second_friday",
+ "created_on": "2025-01-21T07:39:19.798853",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6135,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(UInt8)",
+ "type_generic": 0,
+ "uuid": "27332942-0b3e-45b9-b7bb-7673ebfe9834",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.804709",
+ "column_name": "deleted_flag",
+ "created_on": "2025-01-21T07:39:19.804706",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6136,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(UInt8)",
+ "type_generic": 0,
+ "uuid": "482d2726-0ac0-4d96-bdb4-c85fee7686ad",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.810182",
+ "column_name": "dttm_updated",
+ "created_on": "2025-01-21T07:39:19.810179",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6137,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": null,
+ "type": "Nullable(DateTime)",
+ "type_generic": 2,
+ "uuid": "965ab287-be1d-4ccf-9499-aa756e8369a4",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.815930",
+ "column_name": "filter_date",
+ "created_on": "2025-01-21T07:39:19.815927",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6138,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": null,
+ "type": "Nullable(DateTime)",
+ "type_generic": 2,
+ "uuid": "f96faa56-24ef-4a32-ab7d-a8264d84dc7e",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.821669",
+ "column_name": "dttm_inserted",
+ "created_on": "2025-01-21T07:39:19.821666",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6139,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": null,
+ "type": "Nullable(DateTime)",
+ "type_generic": 2,
+ "uuid": "4e46e715-ff45-4d6d-a9fb-e6dc28fdac08",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.827220",
+ "column_name": "dt_external_contract",
+ "created_on": "2025-01-21T07:39:19.827217",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6140,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": null,
+ "type": "Nullable(Date)",
+ "type_generic": 2,
+ "uuid": "6f0d0df8-d030-4816-913f-24d4f507106b",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.832814",
+ "column_name": "is_fns_restriction_list_exist",
+ "created_on": "2025-01-21T07:39:19.832810",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6141,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "cee2b0e4-aa7d-472e-a9a3-4c098defd74d",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.838345",
+ "column_name": "is_debt_daily_calculated",
+ "created_on": "2025-01-21T07:39:19.838342",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6142,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "10655321-340f-468a-8f98-8d9c3cc5366d",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.843929",
+ "column_name": "unit_balance_code_name",
+ "created_on": "2025-01-21T07:39:19.843926",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6143,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "c7bcedbd-023d-411d-9647-c60f10c99325",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.849508",
+ "column_name": "special_general_ledger_indicator",
+ "created_on": "2025-01-21T07:39:19.849505",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6144,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "a0446f99-c57c-4567-8bf0-a287739fa38f",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.855021",
+ "column_name": "is_group_company_affiliated",
+ "created_on": "2025-01-21T07:39:19.855017",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6145,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "55e45259-1b95-4351-b79f-a8cf3480a46a",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.860821",
+ "column_name": "is_related_party_rsbo",
+ "created_on": "2025-01-21T07:39:19.860818",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6146,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "233e7779-b7d1-4621-81ff-b30365b0123c",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.867178",
+ "column_name": "final_accounting_document_code",
+ "created_on": "2025-01-21T07:39:19.867175",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6147,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "13ced870-020d-4f8f-abc9-b3e6cf2de5f8",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.873085",
+ "column_name": "is_related_party_tco",
+ "created_on": "2025-01-21T07:39:19.873082",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6148,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "603dc460-9136-49ac-899c-bf5a39cb2c15",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.878895",
+ "column_name": "counterparty_search_name",
+ "created_on": "2025-01-21T07:39:19.878892",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6149,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "52229979-269c-4125-93b3-9edd45f23282",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.884623",
+ "column_name": "counterparty_truncated_code",
+ "created_on": "2025-01-21T07:39:19.884620",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6150,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "fc3e15b7-07a8-460a-9776-e114ffc40c70",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.890220",
+ "column_name": "reason_for_reversal",
+ "created_on": "2025-01-21T07:39:19.890217",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6151,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "cc96a82d-a5b8-414b-9de7-5c94300af04a",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.895874",
+ "column_name": "counterparty_mdm_code",
+ "created_on": "2025-01-21T07:39:19.895871",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6152,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "6a7c4678-d93f-45f2-b4cf-cc030e35eda1",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.901564",
+ "column_name": "counterparty_hfm_code",
+ "created_on": "2025-01-21T07:39:19.901561",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6153,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "0fc8b3f9-8951-4d39-8dc7-4c553bd50b66",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.907254",
+ "column_name": "counterparty_tin_code",
+ "created_on": "2025-01-21T07:39:19.907251",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6154,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "a6054696-1a78-4c0b-813d-5d7b97d4be00",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.912884",
+ "column_name": "is_lawsuit_exist",
+ "created_on": "2025-01-21T07:39:19.912881",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6155,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "ba499f9a-5db4-48dc-9ec3-29b36233845f",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.918722",
+ "column_name": "invoice_document_code",
+ "created_on": "2025-01-21T07:39:19.918719",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6156,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "c36da610-62b7-4df6-96e3-3a591b72cf56",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.924283",
+ "column_name": "is_bankrupt",
+ "created_on": "2025-01-21T07:39:19.924280",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6157,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "7ef7ad98-88ed-47c0-ab53-411fb51d260a",
+ "verbose_name": null
+ },
+ {
+ "advanced_data_type": null,
+ "changed_on": "2025-01-21T07:39:19.929991",
+ "column_name": "counterparty_code",
+ "created_on": "2025-01-21T07:39:19.929988",
+ "description": null,
+ "expression": null,
+ "extra": "{\"warning_markdown\":null}",
+ "filterable": true,
+ "groupby": true,
+ "id": 6158,
+ "is_active": true,
+ "is_dttm": false,
+ "python_date_format": null,
+ "type": "Nullable(String)",
+ "type_generic": 1,
+ "uuid": "ffeff11a-5bf3-4eab-a381-08799c59f1bf",
+ "verbose_name": null
+ }
+ ],
+ "created_by": {
+ "first_name": "Андрей",
+ "last_name": "Волобуев"
+ },
+ "created_on": "2025-01-21T07:39:19.316583",
+ "created_on_humanized": "8 месяцев назад",
+ "currency_formats": {},
+ "database": {
+ "backend": "clickhousedb",
+ "database_name": "Dev Clickhouse",
+ "id": 19
+ },
+ "datasource_name": "FI-0022 Штрафы ПДЗ (click)",
+ "datasource_type": "table",
+ "default_endpoint": null,
+ "description": null,
+ "extra": null,
+ "fetch_values_predicate": null,
+ "filter_select_enabled": true,
+ "granularity_sqla": [
+ [
+ "dt_overdue",
+ "dt_overdue"
+ ],
+ [
+ "dt_baseline_due_date_calculation",
+ "dt_baseline_due_date_calculation"
+ ],
+ [
+ "dt_debt",
+ "dt_debt"
+ ],
+ [
+ "dt_accounting_document",
+ "dt_accounting_document"
+ ],
+ [
+ "dt_clearing",
+ "dt_clearing"
+ ],
+ [
+ "dt",
+ "dt"
+ ],
+ [
+ "dttm_updated",
+ "dttm_updated"
+ ],
+ [
+ "filter_date",
+ "filter_date"
+ ],
+ [
+ "dttm_inserted",
+ "dttm_inserted"
+ ],
+ [
+ "dt_external_contract",
+ "dt_external_contract"
+ ]
+ ],
+ "id": 100,
+ "is_managed_externally": false,
+ "is_sqllab_view": false,
+ "kind": "virtual",
+ "main_dttm_col": null,
+ "metrics": [
+ {
+ "changed_on": "2025-01-21T07:39:19.356732",
+ "created_on": "2025-01-21T07:39:19.356729",
+ "currency": null,
+ "d3format": null,
+ "description": null,
+ "expression": "SUM(\ndebt_subposition_document_currency_amount\n)",
+ "extra": "{\"warning_markdown\":\"\"}",
+ "id": 269,
+ "metric_name": "penalty_vd",
+ "metric_type": null,
+ "verbose_name": "Штрафы (ВД)",
+ "warning_text": null
+ },
+ {
+ "changed_on": "2025-01-21T07:39:19.350535",
+ "created_on": "2025-01-21T07:39:19.350532",
+ "currency": null,
+ "d3format": null,
+ "description": null,
+ "expression": "SUM(\ndebt_subposition_local_currency_amount\n)",
+ "extra": "{\"warning_markdown\":\"\"}",
+ "id": 268,
+ "metric_name": "penalty_vv",
+ "metric_type": null,
+ "verbose_name": "Штрафы (ВВ)",
+ "warning_text": null
+ },
+ {
+ "changed_on": "2025-01-21T07:39:19.344771",
+ "created_on": "2025-01-21T07:39:19.344768",
+ "currency": null,
+ "d3format": null,
+ "description": null,
+ "expression": "SUM(\ndebt_balance_subposition_usd_amount\n)",
+ "extra": "{\"warning_markdown\":\"\"}",
+ "id": 267,
+ "metric_name": "penalty_usd",
+ "metric_type": null,
+ "verbose_name": "Штрафы (USD)",
+ "warning_text": null
+ },
+ {
+ "changed_on": "2025-01-21T07:39:19.337884",
+ "created_on": "2025-01-21T07:39:19.337881",
+ "currency": null,
+ "d3format": null,
+ "description": null,
+ "expression": "SUM(\ndebt_subposition_second_local_currency_amount\n)",
+ "extra": "{\"warning_markdown\":\"\"}",
+ "id": 266,
+ "metric_name": "penalty_vv2",
+ "metric_type": null,
+ "verbose_name": "Штрафы (ВВ2)",
+ "warning_text": null
+ }
+ ],
+ "name": "dm.FI-0022 Штрафы ПДЗ (click)",
+ "normalize_columns": false,
+ "offset": 0,
+ "order_by_choices": [
+ [
+ "[\"account_type\", true]",
+ "account_type По возрастанию"
+ ],
+ [
+ "[\"account_type\", false]",
+ "account_type По убыванию"
+ ],
+ [
+ "[\"accounting_document_code\", true]",
+ "accounting_document_code По возрастанию"
+ ],
+ [
+ "[\"accounting_document_code\", false]",
+ "accounting_document_code По убыванию"
+ ],
+ [
+ "[\"accounting_document_status_code\", true]",
+ "accounting_document_status_code По возрастанию"
+ ],
+ [
+ "[\"accounting_document_status_code\", false]",
+ "accounting_document_status_code По убыванию"
+ ],
+ [
+ "[\"accounting_document_type\", true]",
+ "accounting_document_type По возрастанию"
+ ],
+ [
+ "[\"accounting_document_type\", false]",
+ "accounting_document_type По убыванию"
+ ],
+ [
+ "[\"assignment_number\", true]",
+ "assignment_number По возрастанию"
+ ],
+ [
+ "[\"assignment_number\", false]",
+ "assignment_number По убыванию"
+ ],
+ [
+ "[\"budget_subtype_code\", true]",
+ "budget_subtype_code По возрастанию"
+ ],
+ [
+ "[\"budget_subtype_code\", false]",
+ "budget_subtype_code По убыванию"
+ ],
+ [
+ "[\"clearing_document_code\", true]",
+ "clearing_document_code По возрастанию"
+ ],
+ [
+ "[\"clearing_document_code\", false]",
+ "clearing_document_code По убыванию"
+ ],
+ [
+ "[\"contract_number\", true]",
+ "contract_number По возрастанию"
+ ],
+ [
+ "[\"contract_number\", false]",
+ "contract_number По убыванию"
+ ],
+ [
+ "[\"contract_supervisor_employee_number\", true]",
+ "contract_supervisor_employee_number По возрастанию"
+ ],
+ [
+ "[\"contract_supervisor_employee_number\", false]",
+ "contract_supervisor_employee_number По убыванию"
+ ],
+ [
+ "[\"contract_supervisor_name\", true]",
+ "contract_supervisor_name По возрастанию"
+ ],
+ [
+ "[\"contract_supervisor_name\", false]",
+ "contract_supervisor_name По убыванию"
+ ],
+ [
+ "[\"contract_trader_code\", true]",
+ "contract_trader_code По возрастанию"
+ ],
+ [
+ "[\"contract_trader_code\", false]",
+ "contract_trader_code По убыванию"
+ ],
+ [
+ "[\"contract_trader_name\", true]",
+ "contract_trader_name По возрастанию"
+ ],
+ [
+ "[\"contract_trader_name\", false]",
+ "contract_trader_name По убыванию"
+ ],
+ [
+ "[\"counterparty_code\", true]",
+ "counterparty_code По возрастанию"
+ ],
+ [
+ "[\"counterparty_code\", false]",
+ "counterparty_code По убыванию"
+ ],
+ [
+ "[\"counterparty_full_name\", true]",
+ "counterparty_full_name По возрастанию"
+ ],
+ [
+ "[\"counterparty_full_name\", false]",
+ "counterparty_full_name По убыванию"
+ ],
+ [
+ "[\"counterparty_hfm_code\", true]",
+ "counterparty_hfm_code По возрастанию"
+ ],
+ [
+ "[\"counterparty_hfm_code\", false]",
+ "counterparty_hfm_code По убыванию"
+ ],
+ [
+ "[\"counterparty_mdm_code\", true]",
+ "counterparty_mdm_code По возрастанию"
+ ],
+ [
+ "[\"counterparty_mdm_code\", false]",
+ "counterparty_mdm_code По убыванию"
+ ],
+ [
+ "[\"counterparty_search_name\", true]",
+ "counterparty_search_name По возрастанию"
+ ],
+ [
+ "[\"counterparty_search_name\", false]",
+ "counterparty_search_name По убыванию"
+ ],
+ [
+ "[\"counterparty_tin_code\", true]",
+ "counterparty_tin_code По возрастанию"
+ ],
+ [
+ "[\"counterparty_tin_code\", false]",
+ "counterparty_tin_code По убыванию"
+ ],
+ [
+ "[\"counterparty_truncated_code\", true]",
+ "counterparty_truncated_code По возрастанию"
+ ],
+ [
+ "[\"counterparty_truncated_code\", false]",
+ "counterparty_truncated_code По убыванию"
+ ],
+ [
+ "[\"country_code\", true]",
+ "country_code По возрастанию"
+ ],
+ [
+ "[\"country_code\", false]",
+ "country_code По убыванию"
+ ],
+ [
+ "[\"debit_or_credit\", true]",
+ "debit_or_credit По возрастанию"
+ ],
+ [
+ "[\"debit_or_credit\", false]",
+ "debit_or_credit По убыванию"
+ ],
+ [
+ "[\"debt_balance_document_currency_amount\", true]",
+ "debt_balance_document_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_document_currency_amount\", false]",
+ "debt_balance_document_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_exchange_diff_local_currency_amount\", true]",
+ "debt_balance_exchange_diff_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_exchange_diff_local_currency_amount\", false]",
+ "debt_balance_exchange_diff_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_exchange_diff_second_local_currency_amount\", true]",
+ "debt_balance_exchange_diff_second_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_exchange_diff_second_local_currency_amount\", false]",
+ "debt_balance_exchange_diff_second_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_local_currency_amount\", true]",
+ "debt_balance_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_local_currency_amount\", false]",
+ "debt_balance_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_second_local_currency_amount\", true]",
+ "debt_balance_second_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_second_local_currency_amount\", false]",
+ "debt_balance_second_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_subpos_exch_diff_local_currency_amount\", true]",
+ "debt_balance_subpos_exch_diff_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_subpos_exch_diff_local_currency_amount\", false]",
+ "debt_balance_subpos_exch_diff_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_subpos_exch_diff_second_local_curr_amount\", true]",
+ "debt_balance_subpos_exch_diff_second_local_curr_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_subpos_exch_diff_second_local_curr_amount\", false]",
+ "debt_balance_subpos_exch_diff_second_local_curr_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_subpos_second_local_currency_amount_reval\", true]",
+ "debt_balance_subpos_second_local_currency_amount_reval По возрастанию"
+ ],
+ [
+ "[\"debt_balance_subpos_second_local_currency_amount_reval\", false]",
+ "debt_balance_subpos_second_local_currency_amount_reval По убыванию"
+ ],
+ [
+ "[\"debt_balance_subposition_document_currency_amount\", true]",
+ "debt_balance_subposition_document_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_subposition_document_currency_amount\", false]",
+ "debt_balance_subposition_document_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_subposition_local_currency_amount\", true]",
+ "debt_balance_subposition_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_subposition_local_currency_amount\", false]",
+ "debt_balance_subposition_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_subposition_second_local_currency_amount\", true]",
+ "debt_balance_subposition_second_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_subposition_second_local_currency_amount\", false]",
+ "debt_balance_subposition_second_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_subposition_usd_amount\", true]",
+ "debt_balance_subposition_usd_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_subposition_usd_amount\", false]",
+ "debt_balance_subposition_usd_amount По убыванию"
+ ],
+ [
+ "[\"debt_balance_with_revaluation_diff_second_currency_amount\", true]",
+ "debt_balance_with_revaluation_diff_second_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_balance_with_revaluation_diff_second_currency_amount\", false]",
+ "debt_balance_with_revaluation_diff_second_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_period_group\", true]",
+ "debt_period_group По возрастанию"
+ ],
+ [
+ "[\"debt_period_group\", false]",
+ "debt_period_group По убыванию"
+ ],
+ [
+ "[\"debt_subposition_document_currency_amount\", true]",
+ "debt_subposition_document_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_subposition_document_currency_amount\", false]",
+ "debt_subposition_document_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_subposition_local_currency_amount\", true]",
+ "debt_subposition_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_subposition_local_currency_amount\", false]",
+ "debt_subposition_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"debt_subposition_number\", true]",
+ "debt_subposition_number По возрастанию"
+ ],
+ [
+ "[\"debt_subposition_number\", false]",
+ "debt_subposition_number По убыванию"
+ ],
+ [
+ "[\"debt_subposition_second_local_currency_amount\", true]",
+ "debt_subposition_second_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"debt_subposition_second_local_currency_amount\", false]",
+ "debt_subposition_second_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"deleted_flag\", true]",
+ "deleted_flag По возрастанию"
+ ],
+ [
+ "[\"deleted_flag\", false]",
+ "deleted_flag По убыванию"
+ ],
+ [
+ "[\"document_currency_amount\", true]",
+ "document_currency_amount По возрастанию"
+ ],
+ [
+ "[\"document_currency_amount\", false]",
+ "document_currency_amount По убыванию"
+ ],
+ [
+ "[\"document_currency_code\", true]",
+ "document_currency_code По возрастанию"
+ ],
+ [
+ "[\"document_currency_code\", false]",
+ "document_currency_code По убыванию"
+ ],
+ [
+ "[\"dt\", true]",
+ "dt По возрастанию"
+ ],
+ [
+ "[\"dt\", false]",
+ "dt По убыванию"
+ ],
+ [
+ "[\"dt_accounting_document\", true]",
+ "dt_accounting_document По возрастанию"
+ ],
+ [
+ "[\"dt_accounting_document\", false]",
+ "dt_accounting_document По убыванию"
+ ],
+ [
+ "[\"dt_baseline_due_date_calculation\", true]",
+ "dt_baseline_due_date_calculation По возрастанию"
+ ],
+ [
+ "[\"dt_baseline_due_date_calculation\", false]",
+ "dt_baseline_due_date_calculation По убыванию"
+ ],
+ [
+ "[\"dt_clearing\", true]",
+ "dt_clearing По возрастанию"
+ ],
+ [
+ "[\"dt_clearing\", false]",
+ "dt_clearing По убыванию"
+ ],
+ [
+ "[\"dt_debt\", true]",
+ "dt_debt По возрастанию"
+ ],
+ [
+ "[\"dt_debt\", false]",
+ "dt_debt По убыванию"
+ ],
+ [
+ "[\"dt_external_contract\", true]",
+ "dt_external_contract По возрастанию"
+ ],
+ [
+ "[\"dt_external_contract\", false]",
+ "dt_external_contract По убыванию"
+ ],
+ [
+ "[\"dt_overdue\", true]",
+ "dt_overdue По возрастанию"
+ ],
+ [
+ "[\"dt_overdue\", false]",
+ "dt_overdue По убыванию"
+ ],
+ [
+ "[\"dttm_inserted\", true]",
+ "dttm_inserted По возрастанию"
+ ],
+ [
+ "[\"dttm_inserted\", false]",
+ "dttm_inserted По убыванию"
+ ],
+ [
+ "[\"dttm_updated\", true]",
+ "dttm_updated По возрастанию"
+ ],
+ [
+ "[\"dttm_updated\", false]",
+ "dttm_updated По убыванию"
+ ],
+ [
+ "[\"exchange_diff_local_currency_amount\", true]",
+ "exchange_diff_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"exchange_diff_local_currency_amount\", false]",
+ "exchange_diff_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"exchange_diff_second_local_currency_amount\", true]",
+ "exchange_diff_second_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"exchange_diff_second_local_currency_amount\", false]",
+ "exchange_diff_second_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"external_contract_number\", true]",
+ "external_contract_number По возрастанию"
+ ],
+ [
+ "[\"external_contract_number\", false]",
+ "external_contract_number По убыванию"
+ ],
+ [
+ "[\"filter_date\", true]",
+ "filter_date По возрастанию"
+ ],
+ [
+ "[\"filter_date\", false]",
+ "filter_date По убыванию"
+ ],
+ [
+ "[\"final_accounting_document_code\", true]",
+ "final_accounting_document_code По возрастанию"
+ ],
+ [
+ "[\"final_accounting_document_code\", false]",
+ "final_accounting_document_code По убыванию"
+ ],
+ [
+ "[\"final_fiscal_year\", true]",
+ "final_fiscal_year По возрастанию"
+ ],
+ [
+ "[\"final_fiscal_year\", false]",
+ "final_fiscal_year По убыванию"
+ ],
+ [
+ "[\"final_position_line_item\", true]",
+ "final_position_line_item По возрастанию"
+ ],
+ [
+ "[\"final_position_line_item\", false]",
+ "final_position_line_item По убыванию"
+ ],
+ [
+ "[\"fiscal_year\", true]",
+ "fiscal_year По возрастанию"
+ ],
+ [
+ "[\"fiscal_year\", false]",
+ "fiscal_year По убыванию"
+ ],
+ [
+ "[\"fiscal_year_of_relevant_invoice\", true]",
+ "fiscal_year_of_relevant_invoice По возрастанию"
+ ],
+ [
+ "[\"fiscal_year_of_relevant_invoice\", false]",
+ "fiscal_year_of_relevant_invoice По убыванию"
+ ],
+ [
+ "[\"funds_center_code\", true]",
+ "funds_center_code По возрастанию"
+ ],
+ [
+ "[\"funds_center_code\", false]",
+ "funds_center_code По убыванию"
+ ],
+ [
+ "[\"funds_center_name\", true]",
+ "funds_center_name По возрастанию"
+ ],
+ [
+ "[\"funds_center_name\", false]",
+ "funds_center_name По убыванию"
+ ],
+ [
+ "[\"general_ledger_account_code\", true]",
+ "general_ledger_account_code По возрастанию"
+ ],
+ [
+ "[\"general_ledger_account_code\", false]",
+ "general_ledger_account_code По убыванию"
+ ],
+ [
+ "[\"general_ledger_account_full_name\", true]",
+ "general_ledger_account_full_name По возрастанию"
+ ],
+ [
+ "[\"general_ledger_account_full_name\", false]",
+ "general_ledger_account_full_name По убыванию"
+ ],
+ [
+ "[\"invoice_document_code\", true]",
+ "invoice_document_code По возрастанию"
+ ],
+ [
+ "[\"invoice_document_code\", false]",
+ "invoice_document_code По убыванию"
+ ],
+ [
+ "[\"is_bankrupt\", true]",
+ "is_bankrupt По возрастанию"
+ ],
+ [
+ "[\"is_bankrupt\", false]",
+ "is_bankrupt По убыванию"
+ ],
+ [
+ "[\"is_debt_daily_calculated\", true]",
+ "is_debt_daily_calculated По возрастанию"
+ ],
+ [
+ "[\"is_debt_daily_calculated\", false]",
+ "is_debt_daily_calculated По убыванию"
+ ],
+ [
+ "[\"is_fns_restriction_list_exist\", true]",
+ "is_fns_restriction_list_exist По возрастанию"
+ ],
+ [
+ "[\"is_fns_restriction_list_exist\", false]",
+ "is_fns_restriction_list_exist По убыванию"
+ ],
+ [
+ "[\"is_group_company_affiliated\", true]",
+ "is_group_company_affiliated По возрастанию"
+ ],
+ [
+ "[\"is_group_company_affiliated\", false]",
+ "is_group_company_affiliated По убыванию"
+ ],
+ [
+ "[\"is_lawsuit_exist\", true]",
+ "is_lawsuit_exist По возрастанию"
+ ],
+ [
+ "[\"is_lawsuit_exist\", false]",
+ "is_lawsuit_exist По убыванию"
+ ],
+ [
+ "[\"is_related_party_rsbo\", true]",
+ "is_related_party_rsbo По возрастанию"
+ ],
+ [
+ "[\"is_related_party_rsbo\", false]",
+ "is_related_party_rsbo По убыванию"
+ ],
+ [
+ "[\"is_related_party_tco\", true]",
+ "is_related_party_tco По возрастанию"
+ ],
+ [
+ "[\"is_related_party_tco\", false]",
+ "is_related_party_tco По убыванию"
+ ],
+ [
+ "[\"is_second_friday\", true]",
+ "is_second_friday По возрастанию"
+ ],
+ [
+ "[\"is_second_friday\", false]",
+ "is_second_friday По убыванию"
+ ],
+ [
+ "[\"local_currency_amount\", true]",
+ "local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"local_currency_amount\", false]",
+ "local_currency_amount По убыванию"
+ ],
+ [
+ "[\"local_currency_code\", true]",
+ "local_currency_code По возрастанию"
+ ],
+ [
+ "[\"local_currency_code\", false]",
+ "local_currency_code По убыванию"
+ ],
+ [
+ "[\"plant_code\", true]",
+ "plant_code По возрастанию"
+ ],
+ [
+ "[\"plant_code\", false]",
+ "plant_code По убыванию"
+ ],
+ [
+ "[\"plant_code-plant_name\", true]",
+ "plant_code-plant_name По возрастанию"
+ ],
+ [
+ "[\"plant_code-plant_name\", false]",
+ "plant_code-plant_name По убыванию"
+ ],
+ [
+ "[\"plant_name\", true]",
+ "plant_name По возрастанию"
+ ],
+ [
+ "[\"plant_name\", false]",
+ "plant_name По убыванию"
+ ],
+ [
+ "[\"position_line_item\", true]",
+ "position_line_item По возрастанию"
+ ],
+ [
+ "[\"position_line_item\", false]",
+ "position_line_item По убыванию"
+ ],
+ [
+ "[\"position_line_item_text\", true]",
+ "position_line_item_text По возрастанию"
+ ],
+ [
+ "[\"position_line_item_text\", false]",
+ "position_line_item_text По убыванию"
+ ],
+ [
+ "[\"position_number_of_relevant_invoice\", true]",
+ "position_number_of_relevant_invoice По возрастанию"
+ ],
+ [
+ "[\"position_number_of_relevant_invoice\", false]",
+ "position_number_of_relevant_invoice По убыванию"
+ ],
+ [
+ "[\"purchase_or_sales_group_code\", true]",
+ "purchase_or_sales_group_code По возрастанию"
+ ],
+ [
+ "[\"purchase_or_sales_group_code\", false]",
+ "purchase_or_sales_group_code По убыванию"
+ ],
+ [
+ "[\"purchase_or_sales_group_name\", true]",
+ "purchase_or_sales_group_name По возрастанию"
+ ],
+ [
+ "[\"purchase_or_sales_group_name\", false]",
+ "purchase_or_sales_group_name По убыванию"
+ ],
+ [
+ "[\"reason_for_reversal\", true]",
+ "reason_for_reversal По возрастанию"
+ ],
+ [
+ "[\"reason_for_reversal\", false]",
+ "reason_for_reversal По убыванию"
+ ],
+ [
+ "[\"reference_document_number\", true]",
+ "reference_document_number По возрастанию"
+ ],
+ [
+ "[\"reference_document_number\", false]",
+ "reference_document_number По убыванию"
+ ],
+ [
+ "[\"responsibility_center_code\", true]",
+ "responsibility_center_code По возрастанию"
+ ],
+ [
+ "[\"responsibility_center_code\", false]",
+ "responsibility_center_code По убыванию"
+ ],
+ [
+ "[\"responsibility_center_level1_code\", true]",
+ "responsibility_center_level1_code По возрастанию"
+ ],
+ [
+ "[\"responsibility_center_level1_code\", false]",
+ "responsibility_center_level1_code По убыванию"
+ ],
+ [
+ "[\"responsibility_center_level1_name\", true]",
+ "responsibility_center_level1_name По возрастанию"
+ ],
+ [
+ "[\"responsibility_center_level1_name\", false]",
+ "responsibility_center_level1_name По убыванию"
+ ],
+ [
+ "[\"responsibility_center_name\", true]",
+ "responsibility_center_name По возрастанию"
+ ],
+ [
+ "[\"responsibility_center_name\", false]",
+ "responsibility_center_name По убыванию"
+ ],
+ [
+ "[\"reverse_document_code\", true]",
+ "reverse_document_code По возрастанию"
+ ],
+ [
+ "[\"reverse_document_code\", false]",
+ "reverse_document_code По убыванию"
+ ],
+ [
+ "[\"reverse_document_fiscal_year\", true]",
+ "reverse_document_fiscal_year По возрастанию"
+ ],
+ [
+ "[\"reverse_document_fiscal_year\", false]",
+ "reverse_document_fiscal_year По убыванию"
+ ],
+ [
+ "[\"second_local_currency_amount\", true]",
+ "second_local_currency_amount По возрастанию"
+ ],
+ [
+ "[\"second_local_currency_amount\", false]",
+ "second_local_currency_amount По убыванию"
+ ],
+ [
+ "[\"second_local_currency_code\", true]",
+ "second_local_currency_code По возрастанию"
+ ],
+ [
+ "[\"second_local_currency_code\", false]",
+ "second_local_currency_code По убыванию"
+ ],
+ [
+ "[\"special_general_ledger_indicator\", true]",
+ "special_general_ledger_indicator По возрастанию"
+ ],
+ [
+ "[\"special_general_ledger_indicator\", false]",
+ "special_general_ledger_indicator По убыванию"
+ ],
+ [
+ "[\"tax_code\", true]",
+ "tax_code По возрастанию"
+ ],
+ [
+ "[\"tax_code\", false]",
+ "tax_code По убыванию"
+ ],
+ [
+ "[\"terms_of_payment_code\", true]",
+ "terms_of_payment_code По возрастанию"
+ ],
+ [
+ "[\"terms_of_payment_code\", false]",
+ "terms_of_payment_code По убыванию"
+ ],
+ [
+ "[\"terms_of_payment_name\", true]",
+ "terms_of_payment_name По возрастанию"
+ ],
+ [
+ "[\"terms_of_payment_name\", false]",
+ "terms_of_payment_name По убыванию"
+ ],
+ [
+ "[\"unit_balance_code\", true]",
+ "unit_balance_code По возрастанию"
+ ],
+ [
+ "[\"unit_balance_code\", false]",
+ "unit_balance_code По убыванию"
+ ],
+ [
+ "[\"unit_balance_code_name\", true]",
+ "unit_balance_code_name По возрастанию"
+ ],
+ [
+ "[\"unit_balance_code_name\", false]",
+ "unit_balance_code_name По убыванию"
+ ],
+ [
+ "[\"unit_balance_name\", true]",
+ "unit_balance_name По возрастанию"
+ ],
+ [
+ "[\"unit_balance_name\", false]",
+ "unit_balance_name По убыванию"
+ ]
+ ],
+ "owners": [
+ {
+ "first_name": "Андрей",
+ "id": 10,
+ "last_name": "Волобуев"
+ },
+ {
+ "first_name": "admin",
+ "id": 9,
+ "last_name": "admin"
+ }
+ ],
+ "schema": "dm",
+ "select_star": "SELECT *\nFROM `dm`.`FI-0022 Штрафы ПДЗ (click)`\nLIMIT 100",
+ "sql": "select t1.*,\ncase \n when \"dt\" <= \"dt_overdue\" then '0. Дебиторская задолженность'\n when \"dt_overdue\" is null then '0. Дебиторская задолженность'\n when \"dt\" - \"dt_overdue\" between 0 and 5 then '1. ПДЗ до 5 дней'\n when \"dt\" - \"dt_overdue\" between 6 and 15 then '2. ПДЗ до 15 дней'\n when \"dt\" - \"dt_overdue\" between 16 and 30 then '3. ПДЗ до 30 дней'\n when \"dt\" - \"dt_overdue\" between 31 and 60 then '4. ПДЗ до 60 дней'\n when \"dt\" - \"dt_overdue\" between 61 and 90 then '5. ПДЗ до 90 дней'\n when \"dt\" - \"dt_overdue\" > 90 then '6. ПДЗ больше 90 дней'\n\nend as debt_period_group,\nif(is_debt_daily_calculated IS NULL, t1.dt, (now() - INTERVAL 1 DAY)) AS filter_date,\n plant_code || ' ' || plant_name AS \"plant_code-plant_name\",\n unit_balance_code || ' ' || unit_balance_name AS unit_balance_code_name\nfrom\ndm.account_debt_penalty t1\n LEFT JOIN dm.counterparty_td ctd\n ON t1.counterparty_code = ctd.counterparty_code\nwhere ctd.is_deleted IS NULL",
+ "table_name": "FI-0022 Штрафы ПДЗ (click)",
+ "template_params": null,
+ "time_grain_sqla": [
+ [
+ "PT1M",
+ "Минута"
+ ],
+ [
+ "PT5M",
+ "5 минут"
+ ],
+ [
+ "PT10M",
+ "10 минут"
+ ],
+ [
+ "PT15M",
+ "15 минут"
+ ],
+ [
+ "PT30M",
+ "30 минут"
+ ],
+ [
+ "PT1H",
+ "Час"
+ ],
+ [
+ "P1D",
+ "День"
+ ],
+ [
+ "P1W",
+ "Неделя"
+ ],
+ [
+ "P1M",
+ "Месяц"
+ ],
+ [
+ "P3M",
+ "Квартал"
+ ],
+ [
+ "P1Y",
+ "Год"
+ ]
+ ],
+ "uid": "100__table",
+ "url": "/tablemodelview/edit/100",
+ "verbose_map": {
+ "__timestamp": "Time",
+ "account_type": "Вид счета",
+ "accounting_document_code": "Номер бухгалтерского документа",
+ "accounting_document_status_code": "accounting_document_status_code",
+ "accounting_document_type": "Вид документа",
+ "assignment_number": "Номер присвоения",
+ "budget_subtype_code": "Подвид бюджета",
+ "clearing_document_code": "Номер документа выравнивания",
+ "contract_number": "Номер договора",
+ "contract_supervisor_employee_number": "Куратор договора, таб №",
+ "contract_supervisor_name": "Куратор договора, ФИО",
+ "contract_trader_code": "Табельный номер трейдера договора",
+ "contract_trader_name": "ФИО трейдера договора",
+ "counterparty_code": "counterparty_code",
+ "counterparty_full_name": "Наименование контрагента",
+ "counterparty_hfm_code": "counterparty_hfm_code",
+ "counterparty_mdm_code": "counterparty_mdm_code",
+ "counterparty_search_name": "counterparty_search_name",
+ "counterparty_tin_code": "counterparty_tin_code",
+ "counterparty_truncated_code": "counterparty_truncated_code",
+ "country_code": "Страна регистрации контрагента",
+ "debit_or_credit": "Д/К",
+ "debt_balance_document_currency_amount": "Остаток задолженности в валюте документа ",
+ "debt_balance_exchange_diff_local_currency_amount": "ВВ Курсовая разница остатка позиции",
+ "debt_balance_exchange_diff_second_local_currency_amount": "ВВ2 Курсовая разница остатка позиции",
+ "debt_balance_local_currency_amount": "Остаток задолженности в валюте организации",
+ "debt_balance_second_local_currency_amount": "Остаток задолженности во второй валюте",
+ "debt_balance_subpos_exch_diff_local_currency_amount": "debt_balance_subpos_exch_diff_local_currency_amount",
+ "debt_balance_subpos_exch_diff_second_local_curr_amount": "debt_balance_subpos_exch_diff_second_local_curr_amount",
+ "debt_balance_subpos_second_local_currency_amount_reval": "debt_balance_subpos_second_local_currency_amount_reval",
+ "debt_balance_subposition_document_currency_amount": "Остаток КЗ по данной позиции, в валюте документа",
+ "debt_balance_subposition_local_currency_amount": "Остаток КЗ по данной позиции, в валюте БЕ",
+ "debt_balance_subposition_second_local_currency_amount": "Остаток КЗ по данной позиции, во второй валюте",
+ "debt_balance_subposition_usd_amount": "Сумма задолженности подпозиции в USD",
+ "debt_balance_with_revaluation_diff_second_currency_amount": "debt_balance_with_revaluation_diff_second_currency_amount",
+ "debt_period_group": "Период ПДЗ",
+ "debt_subposition_document_currency_amount": "Сумма задолженности подпозиции в валюте документа",
+ "debt_subposition_local_currency_amount": "Сумма задолженности подпозиции в местной валюте",
+ "debt_subposition_number": "Номер подпозиции задолженности",
+ "debt_subposition_second_local_currency_amount": "Сумма задолженности подпозиции во второй местной валюте",
+ "deleted_flag": "deleted_flag",
+ "document_currency_amount": "Сумма в валюте документа",
+ "document_currency_code": "Код валюты документа",
+ "dt": "Дата",
+ "dt_accounting_document": "Дата документа",
+ "dt_baseline_due_date_calculation": "Базовая дата для расчета срока оплаты",
+ "dt_clearing": "Дата выравнивания",
+ "dt_debt": "Дата возникновения задолженности ",
+ "dt_external_contract": "dt_external_contract",
+ "dt_overdue": "Дата, когда задолженность станет просроченной ",
+ "dttm_inserted": "dttm_inserted",
+ "dttm_updated": "dttm_updated",
+ "exchange_diff_local_currency_amount": "exchange_diff_local_currency_amount",
+ "exchange_diff_second_local_currency_amount": "exchange_diff_second_local_currency_amount",
+ "external_contract_number": "Внешний номер договора",
+ "filter_date": "filter_date",
+ "final_accounting_document_code": "final_accounting_document_code",
+ "final_fiscal_year": "final_fiscal_year",
+ "final_position_line_item": "final_position_line_item",
+ "fiscal_year": "Фин. год.",
+ "fiscal_year_of_relevant_invoice": "fiscal_year_of_relevant_invoice",
+ "funds_center_code": "Подразделение финансового менеджмента, код",
+ "funds_center_name": "Подразделение финансового менеджмента, название",
+ "general_ledger_account_code": "Основной счет главной книги ",
+ "general_ledger_account_full_name": "Подробный текст к основному счету на русском",
+ "invoice_document_code": "invoice_document_code",
+ "is_bankrupt": "is_bankrupt",
+ "is_debt_daily_calculated": "is_debt_daily_calculated",
+ "is_fns_restriction_list_exist": "is_fns_restriction_list_exist",
+ "is_group_company_affiliated": "is_group_company_affiliated",
+ "is_lawsuit_exist": "is_lawsuit_exist",
+ "is_related_party_rsbo": "is_related_party_rsbo",
+ "is_related_party_tco": "is_related_party_tco",
+ "is_second_friday": "is_second_friday",
+ "local_currency_amount": "local_currency_amount",
+ "local_currency_code": "Код внутренней валюты",
+ "penalty_usd": "Штрафы (USD)",
+ "penalty_vd": "Штрафы (ВД)",
+ "penalty_vv": "Штрафы (ВВ)",
+ "penalty_vv2": "Штрафы (ВВ2)",
+ "plant_code": "Завод",
+ "plant_code-plant_name": "plant_code-plant_name",
+ "plant_name": "Название филиала",
+ "position_line_item": "Номер строки проводки в рамках бухгалтерского документа ",
+ "position_line_item_text": "Текст к позиции",
+ "position_number_of_relevant_invoice": "position_number_of_relevant_invoice",
+ "purchase_or_sales_group_code": "Группа закупок/сбыта, Код",
+ "purchase_or_sales_group_name": "Группа закупок/сбыта, Наименование",
+ "reason_for_reversal": "reason_for_reversal",
+ "reference_document_number": "Ссылочный номер документа ",
+ "responsibility_center_code": "Центр ответственности, код",
+ "responsibility_center_level1_code": "responsibility_center_level1_code",
+ "responsibility_center_level1_name": "responsibility_center_level1_name",
+ "responsibility_center_name": "Центр ответственности, наименование",
+ "reverse_document_code": "№ документа сторно ",
+ "reverse_document_fiscal_year": "reverse_document_fiscal_year",
+ "second_local_currency_amount": "second_local_currency_amount",
+ "second_local_currency_code": "Код второй внутренней валюты",
+ "special_general_ledger_indicator": "special_general_ledger_indicator",
+ "tax_code": "Код налога с оборота",
+ "terms_of_payment_code": "Код условий платежа",
+ "terms_of_payment_name": "Наименование условия платежа",
+ "unit_balance_code": "Балансовая единица",
+ "unit_balance_code_name": "unit_balance_code_name",
+ "unit_balance_name": "Название БЕ"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tech_spec/Пример PUT.md b/tech_spec/Пример PUT.md
new file mode 100644
index 0000000..73d32a9
--- /dev/null
+++ b/tech_spec/Пример PUT.md
@@ -0,0 +1,57 @@
+put /api/v1/dataset/{pk}
+
+{
+ "cache_timeout": 0,
+ "columns": [
+ {
+ "advanced_data_type": "string",
+ "column_name": "string",
+ "description": "string",
+ "expression": "string",
+ "extra": "string",
+ "filterable": true,
+ "groupby": true,
+ "id": 0,
+ "is_active": true,
+ "is_dttm": true,
+ "python_date_format": "string",
+ "type": "string",
+ "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "verbose_name": "string"
+ }
+ ],
+ "database_id": 0,
+ "default_endpoint": "string",
+ "description": "string",
+ "external_url": "string",
+ "extra": "string",
+ "fetch_values_predicate": "string",
+ "filter_select_enabled": true,
+ "is_managed_externally": true,
+ "is_sqllab_view": true,
+ "main_dttm_col": "string",
+ "metrics": [
+ {
+ "currency": "string",
+ "d3format": "string",
+ "description": "string",
+ "expression": "string",
+ "extra": "string",
+ "id": 0,
+ "metric_name": "string",
+ "metric_type": "string",
+ "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "verbose_name": "string",
+ "warning_text": "string"
+ }
+ ],
+ "normalize_columns": true,
+ "offset": 0,
+ "owners": [
+ 0
+ ],
+ "schema": "string",
+ "sql": "string",
+ "table_name": "string",
+ "template_params": "string"
+}
\ No newline at end of file