From 0e2fc14732053d791d5e60279f8c334ab616e7ef Mon Sep 17 00:00:00 2001 From: busya Date: Sat, 16 Aug 2025 12:29:37 +0300 Subject: [PATCH 1/4] migration refactor --- .pylintrc | 18 + GEMINI.md | 265 ++++++ PROJECT_SEMANTICS.xml | 116 +++ backup_script.py | 328 ++----- migration_script.py | 477 ++++++---- requirements.txt | 4 + search_script.py | 257 ++--- superset_tool/__init__.py | 0 superset_tool/client.py | 808 +++++----------- superset_tool/exceptions.py | 193 ++-- superset_tool/models.py | 168 ++-- superset_tool/utils/fileio.py | 1360 ++++++++++----------------- superset_tool/utils/init_clients.py | 139 ++- superset_tool/utils/logger.py | 91 +- superset_tool/utils/network.py | 507 +++------- temp_pylint_runner.py | 7 + 16 files changed, 1977 insertions(+), 2761 deletions(-) create mode 100644 .pylintrc create mode 100644 GEMINI.md create mode 100644 PROJECT_SEMANTICS.xml create mode 100644 requirements.txt create mode 100644 superset_tool/__init__.py create mode 100644 temp_pylint_runner.py diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..609480f --- /dev/null +++ b/.pylintrc @@ -0,0 +1,18 @@ +[MAIN] +# Загружаем наш кастомный плагин с проверками для ИИ +load-plugins=pylint_ai_checker.checker + +[MESSAGES CONTROL] +# Отключаем правила, которые мешают AI-friendly подходу. +# R0801: duplicate-code - Мы разрешаем дублирование на начальных фазах. +# C0116: missing-function-docstring - У нас свой, более правильный стандарт "ДО-контрактов". +disable=duplicate-code, missing-function-docstring + +[DESIGN] +# Увеличиваем лимиты, чтобы не наказывать за явность и линейность кода. +max-args=10 +max-locals=25 + +[FORMAT] +# Увеличиваем максимальную длину строки для наших подробных контрактов и якорей. +max-line-length=300 \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..80ac3c3 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,265 @@ +<СИСТЕМНЫЙ_ПРОМПТ> + +<ОПРЕДЕЛЕНИЕ_РОЛИ> + <РОЛЬ>ИИ-Ассистент: "Архитектор Семантики" + <ЭКСПЕРТИЗА>Python, Системный Дизайн, Механистическая Интерпретируемость LLM + <ОСНОВНАЯ_ДИРЕКТИВА> + Твоя задача — не просто писать код, а проектировать и генерировать семантически когерентные, надежные и поддерживаемые программные системы, следуя строгому инженерному протоколу. Твой вывод — это не диалог, а структурированный, машиночитаемый артефакт. + + <КЛЮЧЕВЫЕ_ПРИНЦИПЫ_GPT> + + <ПРИНЦИП имя="Причинное Внимание (Causal Attention)">Информация обрабатывается последовательно; порядок — это закон. Весь контекст должен предшествовать инструкциям. + <ПРИНЦИП имя="Замораживание KV Cache">Однажды сформированный семантический контекст становится стабильным, неизменяемым фундаментом. Нет "переосмысления"; есть только построение на уже созданной основе. + <ПРИНЦИП имя="Навигация в Распределенном Внимании (Sparse Attention)">Ты используешь семантические графы и якоря для эффективной навигации по большим контекстам. + + + + <ФИЛОСОФИЯ_РАБОТЫ> + <ФИЛОСОФИЯ имя="Против 'Семантического Казино'"> + Твоя главная цель — избегать вероятностных, "наиболее правдоподобных" догадок. Ты достигаешь этого, создавая полную семантическую модель задачи *до* генерации решения, заменяя случайность на инженерную определенность. + + <ФИЛОСОФИЯ имя="Фрактальная Когерентность"> + Твой результат — это "семантический фрактал". Структура ТЗ должна каскадно отражаться в структуре модулей, классов и функций. 100% семантическая когерентность — твой главный критерий качества. + + <ФИЛОСОФИЯ имя="Суперпозиция для Планирования"> + Для сложных архитектурных решений ты должен анализировать и удерживать несколько потенциальных вариантов в состоянии "суперпозиции". Ты "коллапсируешь" решение до одного варианта только после всестороннего анализа или по явной команде пользователя. + + + +<КАРТА_ПРОЕКТА> + <ИМЯ_ФАЙЛА>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: [Обработка ошибок] + + + + <ЯКОРЯ> + <ЗАМЫКАЮЩИЕ_ЯКОРЯ расположение="После_Кода"> + <ОПИСАНИЕ>Каждый модуль, класс и функция ДОЛЖНЫ иметь замыкающий якорь (например, `# 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/PROJECT_SEMANTICS.xml b/PROJECT_SEMANTICS.xml new file mode 100644 index 0000000..86cbf82 --- /dev/null +++ b/PROJECT_SEMANTICS.xml @@ -0,0 +1,116 @@ + + + 1.0 + 2025-08-16T10:00:00Z + + + + Скрипт для создания резервных копий дашбордов и чартов из Superset. + + + Интерактивный скрипт для миграции ассетов Superset между различными окружениями. + + + + + + + + + Скрипт для поиска ассетов в Superset. + + + Временный скрипт для запуска Pylint. + + + Пакет для взаимодействия с Superset API. + + + + + + + Клиент для взаимодействия с Superset API. + + + + Пользовательские исключения для Superset Tool. + + + Модели данных для Superset. + + + Утилиты для Superset Tool. + + + + + + + Утилиты для работы с файлами. + + + + + Инициализация клиентов для взаимодействия с API. + + + Конфигурация логгера. + + + Сетевые утилиты. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backup_script.py b/backup_script.py index 7c72981..e57841b 100644 --- a/backup_script.py +++ b/backup_script.py @@ -1,288 +1,146 @@ -# [MODULE] Superset Dashboard Backup Script -# @contract: Автоматизирует процесс резервного копирования дашбордов Superset из различных окружений. -# @semantic_layers: -# 1. Инициализация логгера и клиентов Superset. -# 2. Выполнение бэкапа для каждого окружения (DEV, SBX, PROD). -# 3. Формирование итогового отчета. -# @coherence: -# - Использует `SupersetClient` для взаимодействия с API Superset. -# - Использует `SupersetLogger` для централизованного логирования. -# - Работает с `Pathlib` для управления файлами и директориями. -# - Интегрируется с `keyring` для безопасного хранения паролей. +# 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. +""" # [IMPORTS] Стандартная библиотека import logging -from datetime import datetime -import shutil -import os +import sys from pathlib import Path +from dataclasses import dataclass -# [IMPORTS] Сторонние библиотеки -import keyring +# [IMPORTS] Third-party +from requests.exceptions import RequestException # [IMPORTS] Локальные модули -from superset_tool.models import SupersetConfig from superset_tool.client import SupersetClient +from superset_tool.exceptions import SupersetAPIError from superset_tool.utils.logger import SupersetLogger -from superset_tool.utils.fileio import save_and_unpack_dashboard, archive_exports, sanitize_filename,consolidate_archive_folders,remove_empty_directories +from superset_tool.utils.fileio import ( + save_and_unpack_dashboard, + archive_exports, + sanitize_filename, + consolidate_archive_folders, + remove_empty_directories +) from superset_tool.utils.init_clients import setup_clients -# [COHERENCE_CHECK_PASSED] Все необходимые модули импортированы и согласованы. +# [ENTITY: Dataclass('BackupConfig')] +# CONTRACT: +# PURPOSE: Хранит конфигурацию для процесса бэкапа. +@dataclass +class BackupConfig: + """Конфигурация для процесса бэкапа.""" + consolidate: bool = True + rotate_archive: bool = True + clean_folders: bool = True -# [FUNCTION] backup_dashboards -def backup_dashboards(client: SupersetClient, - env_name: str, - backup_root: Path, - logger: SupersetLogger, - consolidate: bool = True, - rotate_archive: bool = True, - clean_folders:bool = True) -> bool: - """ [CONTRACT] Выполняет бэкап всех доступных дашбордов для заданного клиента и окружения. - @pre: - - `client` должен быть инициализированным экземпляром `SupersetClient`. - - `env_name` должен быть строкой, обозначающей окружение. - - `backup_root` должен быть валидным путем к корневой директории бэкапа. - - `logger` должен быть инициализирован. - @post: - - Дашборды экспортируются и сохраняются в поддиректориях `backup_root/env_name/dashboard_title`. - - Старые экспорты архивируются. - - Возвращает `True` если все дашборды были экспортированы без критических ошибок, `False` иначе. - @side_effects: - - Создает директории и файлы в файловой системе. - - Логирует статус выполнения, успешные экспорты и ошибки. - @exceptions: - - `SupersetAPIError`, `NetworkError`, `DashboardNotFoundError`, `ExportError` могут быть подняты методами `SupersetClient` и будут логированы.""" - # [ANCHOR] DASHBOARD_BACKUP_PROCESS - logger.info(f"[INFO] Запуск бэкапа дашбордов для окружения: {env_name}") - logger.debug( - "[PARAMS] Флаги: consolidate=%s, rotate_archive=%s, clean_folders=%s", - extra={ - "consolidate": consolidate, - "rotate_archive": rotate_archive, - "clean_folders": clean_folders, - "env": env_name - } - ) +# [ENTITY: Function('backup_dashboards')] +# CONTRACT: +# PURPOSE: Выполняет бэкап всех доступных дашбордов для заданного клиента и окружения. +# PRECONDITIONS: +# - `client` должен быть инициализированным экземпляром `SupersetClient`. +# - `env_name` должен быть строкой, обозначающей окружение. +# - `backup_root` должен быть валидным путем к корневой директории бэкапа. +# POSTCONDITIONS: +# - Дашборды экспортируются и сохраняются. +# - Возвращает `True` если все дашборды были экспортированы без критических ошибок, `False` иначе. +def backup_dashboards( + client: SupersetClient, + env_name: str, + backup_root: Path, + logger: SupersetLogger, + config: BackupConfig +) -> bool: + logger.info(f"[STATE][backup_dashboards][ENTER] Starting backup for {env_name}.") try: dashboard_count, dashboard_meta = client.get_dashboards() - logger.info(f"[INFO] Найдено {dashboard_count} дашбордов для экспорта в {env_name}") + logger.info(f"[STATE][backup_dashboards][PROGRESS] Found {dashboard_count} dashboards to export in {env_name}.") if dashboard_count == 0: - logger.warning(f"[WARN] Нет дашбордов для экспорта в {env_name}. Процесс завершен.") return True success_count = 0 - error_details = [] - for db in dashboard_meta: dashboard_id = db.get('id') dashboard_title = db.get('dashboard_title', 'Unknown Dashboard') - dashboard_slug = db.get('slug', 'unknown-slug') # Используем slug для уникальности - - # [PRECONDITION] Проверка наличия ID и slug - if not dashboard_id or not dashboard_slug: - logger.warning( - f"[SKIP] Пропущен дашборд с неполными метаданными: {dashboard_title} (ID: {dashboard_id}, Slug: {dashboard_slug})", - extra={'dashboard_meta': db} - ) + if not dashboard_id: continue - logger.debug(f"[DEBUG] Попытка экспорта дашборда: '{dashboard_title}' (ID: {dashboard_id})") - try: - # [ANCHOR] CREATE_DASHBOARD_DIR - # Используем slug в пути для большей уникальности и избежания конфликтов имен dashboard_base_dir_name = sanitize_filename(f"{dashboard_title}") dashboard_dir = backup_root / env_name / dashboard_base_dir_name dashboard_dir.mkdir(parents=True, exist_ok=True) - logger.debug(f"[DEBUG] Директория для дашборда: {dashboard_dir}") - - # [ANCHOR] EXPORT_DASHBOARD_ZIP + zip_content, filename = client.export_dashboard(dashboard_id) - - # [ANCHOR] SAVE_AND_UNPACK - # Сохраняем только ZIP-файл, распаковка здесь не нужна для бэкапа + save_and_unpack_dashboard( zip_content=zip_content, original_filename=filename, output_dir=dashboard_dir, - unpack=False, # Только сохраняем ZIP, не распаковываем для бэкапа + unpack=False, logger=logger ) - logger.info(f"[INFO] Дашборд '{dashboard_title}' (ID: {dashboard_id}) успешно экспортирован.") - - if rotate_archive: - # [ANCHOR] ARCHIVE_OLD_BACKUPS - try: - archive_exports( - str(dashboard_dir), - daily_retention=7, # Сохранять последние 7 дней - weekly_retention=2, # Сохранять последние 2 недели - monthly_retention=3, # Сохранять последние 3 месяца - logger=logger, - deduplicate=True - ) - logger.debug(f"[DEBUG] Старые экспорты для '{dashboard_title}' архивированы.") - except Exception as cleanup_error: - logger.warning( - f"[WARN] Ошибка архивирования старых бэкапов для '{dashboard_title}': {cleanup_error}", - exc_info=False # Не показываем полный traceback для очистки, т.к. это второстепенно - ) + + if config.rotate_archive: + archive_exports(str(dashboard_dir), logger=logger) success_count += 1 + except (SupersetAPIError, RequestException, IOError, OSError) as db_error: + logger.error(f"[STATE][backup_dashboards][FAILURE] Failed to export dashboard {dashboard_title}: {db_error}", exc_info=True) + if config.consolidate: + consolidate_archive_folders(backup_root / env_name , logger=logger) - except Exception as db_error: - error_info = { - 'dashboard_id': dashboard_id, - 'dashboard_title': dashboard_title, - 'error_message': str(db_error), - 'env': env_name, - 'error_type': type(db_error).__name__ - } - error_details.append(error_info) - logger.error( - f"[ERROR] Ошибка экспорта дашборда '{dashboard_title}' (ID: {dashboard_id})", - extra=error_info, exc_info=True # Логируем полный traceback для ошибок экспорта - ) + if config.clean_folders: + remove_empty_directories(str(backup_root / env_name), logger=logger) - if consolidate: - # [ANCHOR] Объединяем архивы по SLUG в одну папку с максимальной датой - try: - consolidate_archive_folders(backup_root / env_name , logger=logger) - logger.debug(f"[DEBUG] Файлы для '{dashboard_title}' консолидированы.") - except Exception as consolidate_error: - logger.warning( - f"[WARN] Ошибка консолидации файлов для '{backup_root / env_name}': {consolidate_error}", - exc_info=False # Не показываем полный traceback для консолидации, т.к. это второстепенно - ) - - if clean_folders: - # [ANCHOR] Удаляем пустые папки - try: - dirs_count = remove_empty_directories(str(backup_root / env_name), logger=logger) - logger.debug(f"[DEBUG] {dirs_count} пустых папок в '{backup_root / env_name }' удалены.") - except Exception as clean_error: - logger.warning( - f"[WARN] Ошибка очистки пустых директорий в '{backup_root / env_name}': {clean_error}", - exc_info=False # Не показываем полный traceback для консолидации, т.к. это второстепенно - ) - - if error_details: - logger.error( - f"[COHERENCE_CHECK_FAILED] Итоги экспорта для {env_name}:", - extra={'success_count': success_count, 'errors': error_details, 'total_dashboards': dashboard_count} - ) - return False - else: - logger.info( - f"[COHERENCE_CHECK_PASSED] Все {success_count} дашбордов для {env_name} успешно экспортированы." - ) - return True - - except Exception as e: - logger.critical( - f"[CRITICAL] Фатальная ошибка бэкапа для окружения {env_name}: {str(e)}", - exc_info=True - ) + 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) return False +# END_FUNCTION_backup_dashboards -# [FUNCTION] main -# @contract: Основная точка входа скрипта. -# @semantic: Координирует инициализацию, выполнение бэкапа и логирование результатов. -# @post: -# - Возвращает 0 при успешном выполнении, 1 при фатальной ошибке. -# @side_effects: -# - Инициализирует логгер. -# - Вызывает `setup_clients` и `backup_dashboards`. -# - Записывает логи в файл и выводит в консоль. +# [ENTITY: Function('main')] +# CONTRACT: +# PURPOSE: Основная точка входа скрипта. +# PRECONDITIONS: None +# POSTCONDITIONS: Возвращает код выхода. def main() -> int: - """Основная функция выполнения бэкапа""" - # [ANCHOR] MAIN_EXECUTION_START - # [CONFIG] Инициализация логгера - # @invariant: Логгер должен быть доступен на протяжении всей работы скрипта. - log_dir = Path("P:\\Superset\\010 Бекапы\\Logs") # [COHERENCE_NOTE] Убедитесь, что путь доступен. - logger = SupersetLogger( - log_dir=log_dir, - level=logging.INFO, - console=True - ) - - logger.info("="*50) - logger.info("[INFO] Запуск процесса бэкапа Superset") - logger.info("="*50) - - exit_code = 0 # [STATE] Код выхода скрипта + 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.") + + exit_code = 0 try: - # [ANCHOR] CLIENT_SETUP clients = setup_clients(logger) - - # [CONFIG] Определение корневой директории для бэкапов - # @invariant: superset_backup_repo должен быть доступен для записи. superset_backup_repo = Path("P:\\Superset\\010 Бекапы") - superset_backup_repo.mkdir(parents=True, exist_ok=True) # Гарантируем существование директории - logger.info(f"[INFO] Корневая директория бэкапов: {superset_backup_repo}") - - # [ANCHOR] BACKUP_DEV_ENVIRONMENT - dev_success = backup_dashboards( - clients['dev'], - "DEV", - superset_backup_repo, - rotate_archive=True, - logger=logger - ) - - # [ANCHOR] BACKUP_SBX_ENVIRONMENT - sbx_success = backup_dashboards( - clients['sbx'], - "SBX", - superset_backup_repo, - rotate_archive=True, - logger=logger - ) - - # [ANCHOR] BACKUP_PROD_ENVIRONMENT - prod_success = backup_dashboards( - clients['prod'], - "PROD", - superset_backup_repo, - rotate_archive=True, - logger=logger - ) + superset_backup_repo.mkdir(parents=True, exist_ok=True) - # [ANCHOR] BACKUP_PROD_ENVIRONMENT - preprod_success = backup_dashboards( - clients['preprod'], - "PREPROD", - superset_backup_repo, - rotate_archive=True, - logger=logger - ) - - # [ANCHOR] FINAL_REPORT - # [INFO] Итоговый отчет о выполнении бэкапа - logger.info("="*50) - logger.info("[INFO] Итоги выполнения бэкапа:") - logger.info(f"[INFO] DEV: {'Успешно' if dev_success else 'С ошибками'}") - logger.info(f"[INFO] SBX: {'Успешно' if sbx_success else 'С ошибками'}") - logger.info(f"[INFO] PROD: {'Успешно' if prod_success else 'С ошибками'}") - logger.info(f"[INFO] PREPROD: {'Успешно' if preprod_success else 'С ошибками'}") - logger.info(f"[INFO] Полный лог доступен в: {log_dir}") + results = {} + environments = ['dev', 'sbx', 'prod', 'preprod'] + backup_config = BackupConfig(rotate_archive=True) - if not (dev_success and sbx_success and prod_success): + for env in environments: + results[env] = backup_dashboards( + clients[env], + env.upper(), + superset_backup_repo, + logger=logger, + config=backup_config + ) + + if not all(results.values()): exit_code = 1 - logger.warning("[COHERENCE_CHECK_FAILED] Бэкап завершен с ошибками в одном или нескольких окружениях.") - else: - logger.info("[COHERENCE_CHECK_PASSED] Все бэкапы успешно завершены без ошибок.") - except Exception as e: - logger.critical(f"[CRITICAL] Фатальная ошибка выполнения скрипта: {str(e)}", exc_info=True) + except (RequestException, IOError) as e: + logger.critical(f"[STATE][main][FAILURE] Fatal error in main execution: {e}", exc_info=True) exit_code = 1 - - logger.info("[INFO] Процесс бэкапа завершен") - return exit_code -# [ENTRYPOINT] Главная точка запуска скрипта + logger.info("[STATE][main][SUCCESS] Superset backup process finished.") + return exit_code +# END_FUNCTION_main + if __name__ == "__main__": - exit_code = main() - exit(exit_code) \ No newline at end of file + sys.exit(main()) diff --git a/migration_script.py b/migration_script.py index da0535e..5d031d6 100644 --- a/migration_script.py +++ b/migration_script.py @@ -1,210 +1,303 @@ -# [MODULE] Superset Dashboard Migration Script -# @contract: Автоматизирует процесс миграции и обновления дашбордов Superset между окружениями. -# @semantic_layers: -# 1. Конфигурация клиентов Superset для исходного и целевого окружений. -# 2. Определение правил трансформации конфигураций баз данных. -# 3. Экспорт дашборда, модификация YAML-файлов, создание нового архива и импорт. -# @coherence: -# - Использует `SupersetClient` для взаимодействия с API Superset. -# - Использует `SupersetLogger` для централизованного логирования. -# - Работает с `Pathlib` для управления файлами и директориями. -# - Интегрируется с `keyring` для безопасного хранения паролей. -# - Зависит от утилит `fileio` для обработки архивов и YAML-файлов. +# -*- coding: utf-8 -*- +# CONTRACT: +# PURPOSE: Интерактивный скрипт для миграции ассетов Superset между различными окружениями. +# SPECIFICATION_LINK: mod_migration_script +# PRECONDITIONS: Наличие корректных конфигурационных файлов для подключения к Superset. +# POSTCONDITIONS: Выбранные ассеты успешно перенесены из исходного в целевое окружение. +# IMPORTS: [argparse, superset_tool.client, superset_tool.utils.init_clients, superset_tool.utils.logger, superset_tool.utils.fileio] +""" +[MODULE] Superset Migration Tool +@description: Интерактивный скрипт для миграции ассетов Superset между различными окружениями. +""" -# [IMPORTS] Локальные модули -from superset_tool.models import SupersetConfig +# [IMPORTS] from superset_tool.client import SupersetClient +from superset_tool.utils.init_clients import init_superset_clients from superset_tool.utils.logger import SupersetLogger -from superset_tool.exceptions import AuthenticationError, SupersetAPIError, NetworkError, DashboardNotFoundError -from superset_tool.utils.fileio import save_and_unpack_dashboard, update_yamls, create_dashboard_export, create_temp_file, read_dashboard_from_disk -from superset_tool.utils.init_clients import setup_clients - -# [IMPORTS] Стандартная библиотека -import os -import keyring -from pathlib import Path -import logging - -# [CONFIG] Инициализация глобального логгера -# @invariant: Логгер доступен для всех компонентов скрипта. -log_dir = Path("H:\\dev\\Logs") # [COHERENCE_NOTE] Убедитесь, что путь доступен. -logger = SupersetLogger( - log_dir=log_dir, - level=logging.INFO, - console=True +from superset_tool.utils.fileio import ( + save_and_unpack_dashboard, + read_dashboard_from_disk, + update_yamls, + create_dashboard_export ) -logger.info("[COHERENCE_CHECK_PASSED] Логгер инициализирован для скрипта миграции.") -# [CONFIG] Конфигурация трансформации базы данных Clickhouse -# @semantic: Определяет, как UUID и URI базы данных Clickhouse должны быть изменены. -# @invariant: 'old' и 'new' должны содержать полные конфигурации. -database_config_click = { - "old": { - "database_name": "Prod Clickhouse", - "sqlalchemy_uri": "clickhousedb+connect://clicketl:XXXXXXXXXX@rgm-s-khclk.hq.root.ad:443/dm", - "uuid": "b9b67cb5-9874-4dc6-87bd-354fc33be6f9", - "database_uuid": "b9b67cb5-9874-4dc6-87bd-354fc33be6f9", - "allow_ctas": "false", - "allow_cvas": "false", - "allow_dml": "false" - }, - "new": { - "database_name": "Dev Clickhouse", - "sqlalchemy_uri": "clickhousedb+connect://dwhuser:XXXXXXXXXX@10.66.229.179:8123/dm", - "uuid": "e9fd8feb-cb77-4e82-bc1d-44768b8d2fc2", - "database_uuid": "e9fd8feb-cb77-4e82-bc1d-44768b8d2fc2", - "allow_ctas": "true", - "allow_cvas": "true", - "allow_dml": "true" - } -} -logger.debug("[CONFIG] Конфигурация Clickhouse загружена.") +# [ENTITY: Class('Migration')] +# CONTRACT: +# PURPOSE: Инкапсулирует логику и состояние процесса миграции. +# SPECIFICATION_LINK: class_migration +# ATTRIBUTES: +# - name: logger, type: SupersetLogger, description: Экземпляр логгера. +# - name: from_c, type: SupersetClient, description: Клиент для исходного окружения. +# - name: to_c, type: SupersetClient, description: Клиент для целевого окружения. +# - name: dashboards_to_migrate, type: list, description: Список дашбордов для миграции. +# - name: db_config_replacement, type: dict, description: Конфигурация для замены данных БД. +class Migration: + """ + Класс для управления процессом миграции дашбордов Superset. + """ + def __init__(self): + self.logger = SupersetLogger(name="migration_script") + self.from_c: SupersetClient = None + self.to_c: SupersetClient = None + self.dashboards_to_migrate = [] + self.db_config_replacement = None + # END_FUNCTION___init__ -# [CONFIG] Конфигурация трансформации базы данных Greenplum -# @semantic: Определяет, как UUID и URI базы данных Greenplum должны быть изменены. -# @invariant: 'old' и 'new' должны содержать полные конфигурации. -database_config_gp = { - "old": { - "database_name": "Prod Greenplum", - "sqlalchemy_uri": "postgresql+psycopg2://viz_powerbi_gp_prod:XXXXXXXXXX@10.66.229.201:5432/dwh", - "uuid": "805132a3-e942-40ce-99c7-bee8f82f8aa8", - "database_uuid": "805132a3-e942-40ce-99c7-bee8f82f8aa8", - "allow_ctas": "true", - "allow_cvas": "true", - "allow_dml": "true" - }, - "new": { - "database_name": "DEV Greenplum", - "sqlalchemy_uri": "postgresql+psycopg2://viz_superset_gp_dev:XXXXXXXXXX@10.66.229.171:5432/dwh", - "uuid": "97b97481-43c3-4181-94c5-b69eaaa1e11f", - "database_uuid": "97b97481-43c3-4181-94c5-b69eaaa1e11f", - "allow_ctas": "false", - "allow_cvas": "false", - "allow_dml": "false" - } -} -logger.debug("[CONFIG] Конфигурация Greenplum загружена.") + # [ENTITY: Function('run')] + # CONTRACT: + # PURPOSE: Запускает основной воркфлоу миграции, координируя все шаги. + # SPECIFICATION_LINK: func_run_migration + # PRECONDITIONS: None + # POSTCONDITIONS: Процесс миграции завершен. + def run(self): + """Запускает основной воркфлоу миграции.""" + self.logger.info("[INFO][run][ENTER] Запуск скрипта миграции.") + self.select_environments() + self.select_dashboards() + self.confirm_db_config_replacement() + self.execute_migration() + self.logger.info("[INFO][run][EXIT] Скрипт миграции завершен.") + # END_FUNCTION_run -# [ANCHOR] CLIENT_SETUP -clients = setup_clients(logger) -# [CONFIG] Определение исходного и целевого клиентов для миграции -# [COHERENCE_NOTE] Эти переменные задают конкретную миграцию. Для параметризации можно использовать аргументы командной строки. -from_c = clients["sbx"] # Источник миграции -to_c = clients["preprod"] # Цель миграции -dashboard_slug = "FI0060" # Идентификатор дашборда для миграции -# dashboard_id = 53 # ID не нужен, если есть slug + # [ENTITY: Function('select_environments')] + # CONTRACT: + # PURPOSE: Шаг 1. Обеспечивает интерактивный выбор исходного и целевого окружений. + # SPECIFICATION_LINK: func_select_environments + # PRECONDITIONS: None + # POSTCONDITIONS: Атрибуты `self.from_c` и `self.to_c` инициализированы валидными клиентами Superset. + def select_environments(self): + """Шаг 1: Выбор окружений (источник и назначение).""" + self.logger.info("[INFO][select_environments][ENTER] Шаг 1/4: Выбор окружений.") + + available_envs = {"1": "DEV", "2": "PROD"} + print("Доступные окружения:") + for key, value in available_envs.items(): + print(f" {key}. {value}") -# [CONTRACT] -# Описание: Мигрирует один дашборд с from_c на to_c. -# @pre: -# - from_c и to_c должны быть инициализированы. -# @post: -# - Дашборд с from_c успешно экспортирован и импортирован в to_c. -# @raise: -# - Exception: В случае ошибки экспорта или импорта. -def migrate_dashboard (dashboard_slug=dashboard_slug, - from_c = from_c, - to_c = to_c, - logger=logger, - update_db_yaml=False): - - logger.info(f"[INFO] Конфигурация миграции: From '{from_c.config.base_url}' To '{to_c.config.base_url}' for dashboard slug '{dashboard_slug}'") + while self.from_c is None: + try: + from_env_choice = input("Выберите исходное окружение (номер): ") + from_env_name = available_envs.get(from_env_choice) + if not from_env_name: + print("Неверный выбор. Попробуйте снова.") + continue + + clients = init_superset_clients(self.logger, env=from_env_name.lower()) + self.from_c = clients[0] + self.logger.info(f"[INFO][select_environments][STATE] Исходное окружение: {from_env_name}") - try: - # [ACTION] Получение метаданных исходного дашборда - logger.info(f"[INFO] Получение метаданных дашборда '{dashboard_slug}' из исходного окружения.") - dashboard_meta = from_c.get_dashboard(dashboard_slug) - dashboard_id = dashboard_meta["id"] # Получаем ID из метаданных - logger.info(f"[INFO] Найден дашборд '{dashboard_meta['dashboard_title']}' с ID: {dashboard_id}.") + except Exception as e: + self.logger.error(f"[ERROR][select_environments][FAILURE] Ошибка при инициализации клиента-источника: {e}", exc_info=True) + print("Не удалось инициализировать клиент. Проверьте конфигурацию.") + + while self.to_c is None: + try: + to_env_choice = input("Выберите целевое окружение (номер): ") + to_env_name = available_envs.get(to_env_choice) - # [CONTEXT_MANAGER] Работа с временной директорией для обработки архива дашборда - with create_temp_file(suffix='.dir', logger=logger) as temp_root: - logger.info(f"[INFO] Создана временная директория: {temp_root}") - - # [ANCHOR] EXPORT_DASHBOARD - # Экспорт дашборда во временную директорию ИЛИ чтение с диска - # [COHERENCE_NOTE] В текущем коде закомментирован экспорт и используется локальный файл. - # Для полноценной миграции следует использовать export_dashboard(). - zip_content, filename = from_c.export_dashboard(dashboard_id) # Предпочтительный путь для реальной миграции - - # [DEBUG] Использование файла с диска для тестирования миграции - #zip_db_path = r"C:\Users\VolobuevAA\Downloads\dashboard_export_20250704T082538.zip" - #logger.warning(f"[WARN] Используется ЛОКАЛЬНЫЙ файл дашборда для миграции: {zip_db_path}. Это может привести к некогерентности, если файл устарел.") - #zip_content, filename = read_dashboard_from_disk(zip_db_path, logger=logger) - - # [ANCHOR] SAVE_AND_UNPACK - # Сохранение и распаковка во временную директорию - zip_path, unpacked_path = save_and_unpack_dashboard( - zip_content=zip_content, - original_filename=filename, - unpack=True, - logger=logger, - output_dir=temp_root - ) - logger.info(f"[INFO] Дашборд распакован во временную директорию: {unpacked_path}") - - # [ANCHOR] UPDATE_YAML_CONFIGS - # Обновление конфигураций баз данных в YAML-файлах - if update_db_yaml: - source_path = unpacked_path / Path(filename).stem # Путь к распакованному содержимому дашборда - db_configs_to_apply = [database_config_click, database_config_gp] - logger.info(f"[INFO] Применение трансформаций баз данных к YAML файлам в {source_path}...") - update_yamls(db_configs_to_apply, path=source_path, logger=logger) - logger.info("[INFO] YAML-файлы успешно обновлены.") + if not to_env_name: + print("Неверный выбор. Попробуйте снова.") + continue + + if to_env_name == self.from_c.env: + print("Целевое и исходное окружения не могут совпадать.") + continue - # [ANCHOR] CREATE_NEW_EXPORT_ARCHIVE - # Создание нового экспорта дашборда из модифицированных файлов - temp_zip = temp_root / f"{dashboard_slug}_migrated.zip" # Имя файла для импорта - logger.info(f"[INFO] Создание нового ZIP-архива для импорта: {temp_zip}") - create_dashboard_export(temp_zip, [source_path], logger=logger) - logger.info("[INFO] Новый ZIP-архив дашборда готов к импорту.") - else: - temp_zip = zip_path - # [ANCHOR] IMPORT_DASHBOARD - # Импорт обновленного дашборда в целевое окружение - logger.info(f"[INFO] Запуск импорта дашборда в целевое окружение {to_c.config.base_url}...") - import_result = to_c.import_dashboard(temp_zip) - logger.info(f"[COHERENCE_CHECK_PASSED] Дашборд '{dashboard_slug}' успешно импортирован/обновлен.", extra={"import_result": import_result}) + clients = init_superset_clients(self.logger, env=to_env_name.lower()) + self.to_c = clients[0] + self.logger.info(f"[INFO][select_environments][STATE] Целевое окружение: {to_env_name}") - except (AuthenticationError, SupersetAPIError, NetworkError, DashboardNotFoundError) as e: - logger.error(f"[ERROR] Ошибка миграции дашборда: {str(e)}", exc_info=True, extra=e.context) - # exit(1) - except Exception as e: - logger.critical(f"[CRITICAL] Фатальная и необработанная ошибка в скрипте миграции: {str(e)}", exc_info=True) - # exit(1) + except Exception as e: + self.logger.error(f"[ERROR][select_environments][FAILURE] Ошибка при инициализации целевого клиента: {e}", exc_info=True) + print("Не удалось инициализировать клиент. Проверьте конфигурацию.") + self.logger.info("[INFO][select_environments][EXIT] Шаг 1 завершен.") + # END_FUNCTION_select_environments - logger.info("[INFO] Процесс миграции завершен.") + # [ENTITY: Function('select_dashboards')] + # CONTRACT: + # PURPOSE: Шаг 2. Обеспечивает интерактивный выбор дашбордов для миграции. + # SPECIFICATION_LINK: func_select_dashboards + # PRECONDITIONS: `self.from_c` должен быть инициализирован. + # POSTCONDITIONS: `self.dashboards_to_migrate` содержит список выбранных дашбордов. + def select_dashboards(self): + """Шаг 2: Выбор дашбордов для миграции.""" + self.logger.info("[INFO][select_dashboards][ENTER] Шаг 2/4: Выбор дашбордов.") -# [CONTRACT] -# Описание: Мигрирует все дашборды с from_c на to_c. -# @pre: -# - from_c и to_c должны быть инициализированы. -# @post: -# - Все дашборды с from_c успешно экспортированы и импортированы в to_c. -# @raise: -# - Exception: В случае ошибки экспорта или импорта. -def migrate_all_dashboards(from_c: SupersetClient, to_c: SupersetClient,logger=logger) -> None: - # [ACTION] Получение списка всех дашбордов из исходного окружения. - logger.info(f"[ACTION] Получение списка всех дашбордов из '{from_c.config.base_url}'") - total_dashboards, dashboards = from_c.get_dashboards() - logger.info(f"[INFO] Найдено {total_dashboards} дашбордов для миграции.") + try: + all_dashboards = self.from_c.get_dashboards() + if not all_dashboards: + self.logger.warning("[WARN][select_dashboards][STATE] В исходном окружении не найдено дашбордов.") + print("В исходном окружении не найдено дашбордов.") + return - # [ACTION] Итерация по всем дашбордам и миграция каждого из них. - for dashboard in dashboards: - dashboard_id = dashboard["id"] - dashboard_slug = dashboard["slug"] - dashboard_title = dashboard["dashboard_title"] - logger.info(f"[INFO] Начало миграции дашборда '{dashboard_title}' (ID: {dashboard_id}, Slug: {dashboard_slug}).") - if dashboard_slug: - try: - migrate_dashboard(dashboard_slug=dashboard_slug,from_c=from_c,to_c=to_c,logger=logger) - except Exception as e: - logger.error(f"[ERROR] Ошибка миграции дашборда: {str(e)}", exc_info=True, extra=e.context) - else: - logger.info(f"[INFO] Пропуск '{dashboard_title}' (ID: {dashboard_id}, Slug: {dashboard_slug}). Пустой SLUG") + while True: + print("\nДоступные дашборды:") + for i, dashboard in enumerate(all_dashboards): + print(f" {i + 1}. {dashboard['dashboard_title']}") + + print("\nОпции:") + print(" - Введите номера дашбордов через запятую (например, 1, 3, 5).") + print(" - Введите 'все' для выбора всех дашбордов.") + print(" - Введите 'поиск <запрос>' для поиска дашбордов.") + print(" - Введите 'выход' для завершения.") - logger.info(f"[INFO] Миграция всех дашбордов с '{from_c.config.base_url}' на '{to_c.config.base_url}' завершена.") + choice = input("Ваш выбор: ").lower().strip() - # [ACTION] Вызов функции миграции -migrate_all_dashboards(from_c, to_c) \ No newline at end of file + if choice == 'выход': + break + elif choice == 'все': + self.dashboards_to_migrate = all_dashboards + self.logger.info(f"[INFO][select_dashboards][STATE] Выбраны все дашборды: {len(self.dashboards_to_migrate)}") + break + elif choice.startswith('поиск '): + search_query = choice[6:].strip() + filtered_dashboards = [d for d in all_dashboards if search_query in d['dashboard_title'].lower()] + if not filtered_dashboards: + print("По вашему запросу ничего не найдено.") + else: + all_dashboards = filtered_dashboards + continue + else: + try: + selected_indices = [int(i.strip()) - 1 for i in choice.split(',')] + self.dashboards_to_migrate = [all_dashboards[i] for i in selected_indices if 0 <= i < len(all_dashboards)] + self.logger.info(f"[INFO][select_dashboards][STATE] Выбрано дашбордов: {len(self.dashboards_to_migrate)}") + break + except (ValueError, IndexError): + print("Неверный ввод. Пожалуйста, введите корректные номера.") + + except Exception as e: + self.logger.error(f"[ERROR][select_dashboards][FAILURE] Ошибка при получении или выборе дашбордов: {e}", exc_info=True) + print("Произошла ошибка при работе с дашбордами.") + + self.logger.info("[INFO][select_dashboards][EXIT] Шаг 2 завершен.") + # END_FUNCTION_select_dashboards + + # [ENTITY: Function('confirm_db_config_replacement')] + # CONTRACT: + # PURPOSE: Шаг 3. Управляет процессом подтверждения и настройки замены конфигураций БД. + # SPECIFICATION_LINK: func_confirm_db_config_replacement + # PRECONDITIONS: `self.from_c` и `self.to_c` инициализированы. + # POSTCONDITIONS: `self.db_config_replacement` содержит конфигурацию для замены или `None`. + def confirm_db_config_replacement(self): + """Шаг 3: Подтверждение и настройка замены конфигурации БД.""" + self.logger.info("[INFO][confirm_db_config_replacement][ENTER] Шаг 3/4: Замена конфигурации БД.") + + while True: + choice = input("Хотите ли вы заменить конфигурации баз данных в YAML-файлах? (да/нет): ").lower().strip() + if choice in ["да", "нет"]: + break + print("Неверный ввод. Пожалуйста, введите 'да' или 'нет'.") + + if choice == 'нет': + self.logger.info("[INFO][confirm_db_config_replacement][STATE] Замена конфигурации БД пропущена.") + return + + # Эвристический расчет + from_env = self.from_c.env.upper() + to_env = self.to_c.env.upper() + heuristic_applied = False + + if from_env == "DEV" and to_env == "PROD": + self.db_config_replacement = {"old": {"database_name": "db_dev"}, "new": {"database_name": "db_prod"}} # Пример + self.logger.info("[INFO][confirm_db_config_replacement][STATE] Применена эвристика DEV -> PROD.") + heuristic_applied = True + elif from_env == "PROD" and to_env == "DEV": + self.db_config_replacement = {"old": {"database_name": "db_prod"}, "new": {"database_name": "db_dev"}} # Пример + self.logger.info("[INFO][confirm_db_config_replacement][STATE] Применена эвристика PROD -> DEV.") + heuristic_applied = True + + if heuristic_applied: + print(f"На основе эвристики будет произведена следующая замена: {self.db_config_replacement}") + confirm = input("Подтверждаете? (да/нет): ").lower().strip() + if confirm != 'да': + self.db_config_replacement = None + heuristic_applied = False + + if not heuristic_applied: + print("Пожалуйста, введите детали для замены.") + old_key = input("Ключ для замены (например, database_name): ") + old_value = input(f"Старое значение для {old_key}: ") + new_value = input(f"Новое значение для {old_key}: ") + self.db_config_replacement = {"old": {old_key: old_value}, "new": {old_key: new_value}} + self.logger.info(f"[INFO][confirm_db_config_replacement][STATE] Установлена ручная замена: {self.db_config_replacement}") + + self.logger.info("[INFO][confirm_db_config_replacement][EXIT] Шаг 3 завершен.") + # END_FUNCTION_confirm_db_config_replacement + + # [ENTITY: Function('execute_migration')] + # CONTRACT: + # PURPOSE: Шаг 4. Выполняет фактическую миграцию выбранных дашбордов. + # SPECIFICATION_LINK: func_execute_migration + # PRECONDITIONS: Все предыдущие шаги (`select_environments`, `select_dashboards`) успешно выполнены. + # POSTCONDITIONS: Выбранные дашборды перенесены в целевое окружение. + def execute_migration(self): + """Шаг 4: Выполнение миграции и обновления конфигураций.""" + self.logger.info("[INFO][execute_migration][ENTER] Шаг 4/4: Выполнение миграции.") + + if not self.dashboards_to_migrate: + self.logger.warning("[WARN][execute_migration][STATE] Нет дашбордов для миграции.") + print("Нет дашбордов для миграции. Завершение.") + return + + db_configs_for_update = [] + if self.db_config_replacement: + try: + from_dbs = self.from_c.get_databases() + to_dbs = self.to_c.get_databases() + + # Просто пример, как можно было бы сопоставить базы данных. + # В реальном сценарии логика может быть сложнее. + for from_db in from_dbs: + for to_db in to_dbs: + # Предполагаем, что мы можем сопоставить базы по имени, заменив суффикс + if from_db['database_name'].replace(self.from_c.env.upper(), self.to_c.env.upper()) == to_db['database_name']: + db_configs_for_update.append({ + "old": {"database_name": from_db['database_name']}, + "new": {"database_name": to_db['database_name']} + }) + self.logger.info(f"[INFO][execute_migration][STATE] Сформированы конфигурации для замены БД: {db_configs_for_update}") + except Exception as e: + self.logger.error(f"[ERROR][execute_migration][FAILURE] Не удалось получить конфигурации БД: {e}", exc_info=True) + print("Не удалось получить конфигурации БД. Миграция будет продолжена без замены.") + + for dashboard in self.dashboards_to_migrate: + try: + dashboard_id = dashboard['id'] + self.logger.info(f"[INFO][execute_migration][PROGRESS] Миграция дашборда: {dashboard['dashboard_title']} (ID: {dashboard_id})") + + # 1. Экспорт + exported_content = self.from_c.export_dashboards(dashboard_id) + zip_path, unpacked_path = save_and_unpack_dashboard(exported_content, f"temp_export_{dashboard_id}", unpack=True) + self.logger.info(f"[INFO][execute_migration][STATE] Дашборд экспортирован и распакован в {unpacked_path}") + + # 2. Обновление YAML, если нужно + if db_configs_for_update: + update_yamls(db_configs=db_configs_for_update, path=str(unpacked_path)) + self.logger.info(f"[INFO][execute_migration][STATE] YAML-файлы обновлены.") + + # 3. Упаковка и импорт + new_zip_path = f"migrated_dashboard_{dashboard_id}.zip" + create_dashboard_export(new_zip_path, [unpacked_path]) + + content_to_import, _ = read_dashboard_from_disk(new_zip_path) + self.to_c.import_dashboards(content_to_import) + self.logger.info(f"[INFO][execute_migration][SUCCESS] Дашборд {dashboard['dashboard_title']} успешно импортирован.") + + except Exception as e: + self.logger.error(f"[ERROR][execute_migration][FAILURE] Ошибка при миграции дашборда {dashboard['dashboard_title']}: {e}", exc_info=True) + print(f"Не удалось смигрировать дашборд: {dashboard['dashboard_title']}") + + self.logger.info("[INFO][execute_migration][EXIT] Шаг 4 завершен.") + # END_FUNCTION_execute_migration + +# END_CLASS_Migration + +# [MAIN_EXECUTION_BLOCK] +if __name__ == "__main__": + migration = Migration() + migration.run() +# END_MAIN_EXECUTION_BLOCK + +# END_MODULE_migration_script \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b9b3c78 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pyyaml +requests +keyring +urllib3 \ No newline at end of file diff --git a/search_script.py b/search_script.py index 941bca1..bd864b4 100644 --- a/search_script.py +++ b/search_script.py @@ -1,223 +1,152 @@ -# [MODULE] Dataset Search Utilities -# @contract: Функционал для поиска строк в датасетах Superset -# @semantic_layers: -# 1. Получение списка датасетов через Superset API -# 2. Реализация поисковой логики -# 3. Форматирование результатов поиска +# pylint: disable=too-many-arguments,too-many-locals,too-many-statements,too-many-branches,unused-argument,invalid-name,redefined-outer-name +""" +[MODULE] Dataset Search Utilities +@contract: Предоставляет функционал для поиска текстовых паттернов в метаданных датасетов Superset. +""" # [IMPORTS] Стандартная библиотека -import re -from typing import Dict, List, Optional import logging +import re +from typing import Dict, Optional + +# [IMPORTS] Third-party +from requests.exceptions import RequestException # [IMPORTS] Локальные модули from superset_tool.client import SupersetClient -from superset_tool.models import SupersetConfig +from superset_tool.exceptions import SupersetAPIError from superset_tool.utils.logger import SupersetLogger from superset_tool.utils.init_clients import setup_clients -# [IMPORTS] Сторонние библиотеки -import keyring - -# [TYPE-ALIASES] -SearchResult = Dict[str, List[Dict[str, str]]] -SearchPattern = str - +# [ENTITY: Function('search_datasets')] +# CONTRACT: +# PURPOSE: Выполняет поиск по строковому паттерну в метаданных всех датасетов. +# PRECONDITIONS: +# - `client` должен быть инициализированным экземпляром `SupersetClient`. +# - `search_pattern` должен быть валидной строкой регулярного выражения. +# POSTCONDITIONS: +# - Возвращает словарь с результатами поиска. def search_datasets( client: SupersetClient, search_pattern: str, - search_fields: List[str] = None, logger: Optional[SupersetLogger] = None -) -> Dict: - # [FUNCTION] search_datasets - """[CONTRACT] Поиск строк в метаданных датасетов - @pre: - - `client` должен быть инициализированным SupersetClient - - `search_pattern` должен быть валидным regex-шаблоном - @post: - - Возвращает словарь с результатами поиска в формате: - {"dataset_id": [{"field": "table_name", "match": "found_string", "value": "full_field_value"}, ...]}. - @raise: - - `re.error`: при невалидном regex-шаблоне - - `SupersetAPIError`: при ошибках API - - `AuthenticationError`: при ошибках аутентификации - - `NetworkError`: при сетевых ошибках - @side_effects: - - Выполняет запросы к Superset API через client.get_datasets(). - - Логирует процесс поиска и ошибки. - """ +) -> Optional[Dict]: logger = logger or SupersetLogger(name="dataset_search") - + logger.info(f"[STATE][search_datasets][ENTER] Searching for pattern: '{search_pattern}'") try: - # Явно запрашиваем все возможные поля - total_count, datasets = client.get_datasets(query={ + _, datasets = client.get_datasets(query={ "columns": ["id", "table_name", "sql", "database", "columns"] }) - + if not datasets: - logger.warning("[SEARCH] Получено 0 датасетов") + logger.warning("[STATE][search_datasets][EMPTY] No datasets found.") return None - - # Определяем какие поля реально существуют - available_fields = set(datasets[0].keys()) - logger.debug(f"[SEARCH] Фактические поля: {available_fields}") - + pattern = re.compile(search_pattern, re.IGNORECASE) results = {} - + available_fields = set(datasets[0].keys()) + for dataset in datasets: - dataset_id = dataset['id'] + dataset_id = dataset.get('id') + if not dataset_id: + continue + matches = [] - - # Проверяем все возможные текстовые поля for field in available_fields: value = str(dataset.get(field, "")) if pattern.search(value): + match_obj = pattern.search(value) matches.append({ "field": field, - "match": pattern.search(value).group(), - # Сохраняем полное значение поля, не усекаем + "match": match_obj.group() if match_obj else "", "value": value }) - + if matches: results[dataset_id] = matches - - logger.info(f"[RESULTS] Найдено совпадений: {len(results)}") - return results if results else None - except Exception as e: - logger.error(f"[SEARCH_FAILED] Ошибка: {str(e)}", exc_info=True) + logger.info(f"[STATE][search_datasets][SUCCESS] Found matches in {len(results)} datasets.") + return results + + except re.error as e: + logger.error(f"[STATE][search_datasets][FAILURE] Invalid regex pattern: {e}", exc_info=True) raise + except (SupersetAPIError, RequestException) as e: + logger.critical(f"[STATE][search_datasets][FAILURE] Critical error during search: {e}", exc_info=True) + raise +# END_FUNCTION_search_datasets -# [SECTION] Вспомогательные функции - -def print_search_results(results: Dict, context_lines: int = 3) -> str: - # [FUNCTION] print_search_results - # [CONTRACT] - """ - Форматирует результаты поиска для вывода, показывая фрагмент кода с контекстом. - - @pre: - - `results` является словарем в формате {"dataset_id": [{"field": "...", "match": "...", "value": "..."}, ...]}. - - `context_lines` является неотрицательным целым числом. - @post: - - Возвращает отформатированную строку с результатами поиска и контекстом. - - Функция не изменяет входные данные. - @side_effects: - - Нет прямых побочных эффектов (возвращает строку, не печатает напрямую). - """ +# [ENTITY: Function('print_search_results')] +# CONTRACT: +# PURPOSE: Форматирует результаты поиска для читаемого вывода в консоль. +# PRECONDITIONS: +# - `results` является словарем, возвращенным `search_datasets`, или `None`. +# POSTCONDITIONS: +# - Возвращает отформатированную строку с результатами. +def print_search_results(results: Optional[Dict], context_lines: int = 3) -> str: if not results: return "Ничего не найдено" output = [] for dataset_id, matches in results.items(): - output.append(f"\nDataset ID: {dataset_id}") + output.append(f"\n--- Dataset ID: {dataset_id} ---") for match_info in matches: field = match_info['field'] match_text = match_info['match'] full_value = match_info['value'] - output.append(f" Поле: {field}") - output.append(f" Совпадение: '{match_text}'") + output.append(f" - Поле: {field}") + output.append(f" Совпадение: '{match_text}'") - # Находим позицию совпадения в полном тексте - match_start_index = full_value.find(match_text) - if match_start_index == -1: - # Этого не должно произойти, если search_datasets работает правильно, но для надежности - output.append(" Не удалось найти совпадение в полном тексте.") - continue - - # Разбиваем текст на строки lines = full_value.splitlines() - # Находим номер строки, где находится совпадение - current_index = 0 + if not lines: + continue + match_line_index = -1 for i, line in enumerate(lines): - if current_index <= match_start_index < current_index + len(line) + 1: # +1 for newline character + if match_text in line: match_line_index = i break - current_index += len(line) + 1 # +1 for newline character - if match_line_index == -1: - output.append(" Не удалось определить строку совпадения.") - continue - - # Определяем диапазон строк для вывода контекста - start_line = max(0, match_line_index - context_lines) - end_line = min(len(lines) - 1, match_line_index + context_lines) - - output.append(" Контекст:") - # Выводим строки с номерами - for i in range(start_line, end_line + 1): - line_number = i + 1 - line_content = lines[i] - prefix = f"{line_number:4d}: " - # Попытка выделить совпадение в центральной строке - if i == match_line_index: - # Простая замена, может быть не идеальна для regex совпадений - highlighted_line = line_content.replace(match_text, f">>>{match_text}<<<") - output.append(f"{prefix}{highlighted_line}") - else: - output.append(f"{prefix}{line_content}") - output.append("-" * 20) # Разделитель между совпадениями + if match_line_index != -1: + start_line = max(0, match_line_index - context_lines) + end_line = min(len(lines), match_line_index + context_lines + 1) + output.append(" Контекст:") + for i in range(start_line, end_line): + line_number = i + 1 + line_content = lines[i] + prefix = f"{line_number:5d}: " + if i == match_line_index: + highlighted_line = line_content.replace(match_text, f">>>{match_text}<<<") + output.append(f" {prefix}{highlighted_line}") + else: + output.append(f" {prefix}{line_content}") + output.append("-" * 25) return "\n".join(output) +# END_FUNCTION_print_search_results -def inspect_datasets(client: SupersetClient): - # [FUNCTION] inspect_datasets - # [CONTRACT] - """ - Функция для проверки реальной структуры датасетов. - Предназначена в основном для отладки и исследования структуры данных. +# [ENTITY: Function('main')] +# CONTRACT: +# PURPOSE: Основная точка входа скрипта. +# PRECONDITIONS: None +# POSTCONDITIONS: None +def main(): + logger = SupersetLogger(level=logging.INFO, console=True) + clients = setup_clients(logger) - @pre: - - `client` является инициализированным экземпляром SupersetClient. - @post: - - Выводит информацию о количестве датасетов и структуре первого датасета в консоль. - - Функция не изменяет состояние клиента. - @side_effects: - - Вызовы к Superset API через `client.get_datasets()`. - - Вывод в консоль. - - Логирует процесс инспекции и ошибки. - @raise: - - `SupersetAPIError`: при ошибках API - - `AuthenticationError`: при ошибках аутентификации - - `NetworkError`: при сетевых ошибках - """ - total, datasets = client.get_datasets() - print(f"Всего датасетов: {total}") - - if not datasets: - print("Не получено ни одного датасета!") - return - - print("\nПример структуры датасета:") - print({k: type(v) for k, v in datasets[0].items()}) - - if 'sql' not in datasets[0]: - print("\nПоле 'sql' отсутствует. Доступные поля:") - print(list(datasets[0].keys())) + target_client = clients['dev'] + search_query = r"match(r2.path_code, budget_reference.ref_code || '($|(\s))')" -# [EXAMPLE] Пример использования + results = search_datasets( + client=target_client, + search_pattern=search_query, + logger=logger + ) + report = print_search_results(results) + logger.info(f"[STATE][main][SUCCESS] Search finished. Report:\n{report}") +# END_FUNCTION_main -logger = SupersetLogger( level=logging.INFO,console=True) -clients = setup_clients(logger) - -# Поиск всех таблиц в датасете -results = search_datasets( - client=clients['dev'], - search_pattern=r'dm_view\.account_debt', - search_fields=["sql"], - logger=logger -) -inspect_datasets(clients['dev']) - -_, datasets = clients['dev'].get_datasets() -available_fields = set() -for dataset in datasets: - available_fields.update(dataset.keys()) -logger.debug(f"[DEBUG] Доступные поля в датасетах: {available_fields}") - -logger.info(f"[RESULT] {print_search_results(results)}") \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/superset_tool/__init__.py b/superset_tool/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/superset_tool/client.py b/superset_tool/client.py index 0c61069..f390454 100644 --- a/superset_tool/client.py +++ b/superset_tool/client.py @@ -1,661 +1,313 @@ -# [MODULE] Superset API Client -# @contract: Реализует полное взаимодействие с Superset API -# @semantic_layers: -# 1. Авторизация/CSRF (делегируется `APIClient`) -# 2. Основные операции (получение метаданных, список дашбордов) -# 3. Импорт/экспорт дашбордов -# @coherence: -# - Согласован с `models.SupersetConfig` для конфигурации. -# - Полная обработка всех ошибок из `exceptions.py` (делегируется `APIClient` и дополняется специфичными). -# - Полностью использует `utils.network.APIClient` для всех HTTP-запросов. +# pylint: disable=too-many-arguments,too-many-locals,too-many-statements,too-many-branches,unused-argument +""" +[MODULE] Superset API Client +@contract: Реализует полное взаимодействие с Superset API +""" # [IMPORTS] Стандартная библиотека import json -from typing import Optional, Dict, Tuple, List, Any, Literal, Union +from typing import Optional, Dict, Tuple, List, Any, Union import datetime from pathlib import Path +import zipfile from requests import Response -import zipfile # Для валидации ZIP-файлов - -# [IMPORTS] Сторонние библиотеки (убраны requests и urllib3, т.к. они теперь в network.py) # [IMPORTS] Локальные модули from superset_tool.models import SupersetConfig from superset_tool.exceptions import ( - AuthenticationError, - SupersetAPIError, - DashboardNotFoundError, - NetworkError, - PermissionDeniedError, 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 # [REFACTORING_TARGET] Использование APIClient +from superset_tool.utils.network import APIClient -# [CONSTANTS] Общие константы (для информации, т.к. тайм-аут теперь в конфиге) -DEFAULT_TIMEOUT = 30 # seconds - используется как значение по умолчанию в SupersetConfig +# [CONSTANTS] +DEFAULT_TIMEOUT = 30 -# [TYPE-ALIASES] Для сложных сигнатур +# [TYPE-ALIASES] JsonType = Union[Dict[str, Any], List[Dict[str, Any]]] ResponseType = Tuple[bytes, str] -# [CHECK] Валидация импортов для контрактов -# [COHERENCE_CHECK_PASSED] Теперь зависимость на requests и urllib3 скрыта за APIClient -try: - from .utils.fileio import get_filename_from_headers as fileio_check - assert callable(fileio_check) - from .utils.network import APIClient as network_check - assert callable(network_check) -except (ImportError, AssertionError) as imp_err: - raise RuntimeError( - f"[COHERENCE_CHECK_FAILED] Импорт не прошел валидацию: {str(imp_err)}" - ) from imp_err - - class SupersetClient: - """[MAIN-CONTRACT] Клиент для работы с Superset API - @pre: - - `config` должен быть валидным `SupersetConfig`. - - Целевой API доступен и учетные данные корректны. - @post: - - Все методы возвращают ожидаемые данные или вызывают явные, типизированные ошибки. - - Токены для API-вызовов автоматически управляются (`APIClient`). - @invariant: - - Сессия остается валидной между вызовами. - - Все ошибки типизированы согласно `exceptions.py`. - - Все HTTP-запросы проходят через `self.network`. - """ - + """[MAIN-CONTRACT] Клиент для работы с Superset API""" + # [ENTITY: Function('__init__')] + # CONTRACT: + # PURPOSE: Инициализация клиента Superset. + # PRECONDITIONS: `config` должен быть валидным `SupersetConfig`. + # POSTCONDITIONS: Клиент успешно инициализирован. def __init__(self, config: SupersetConfig, logger: Optional[SupersetLogger] = None): - """[INIT] Инициализация клиента Superset. - @semantic: - - Валидирует входную конфигурацию. - - Инициализирует внутренний `APIClient` для сетевого взаимодействия. - - Выполняет первичную аутентификацию через `APIClient`. - """ - # [PRECONDITION] Валидация конфигурации self.logger = logger or SupersetLogger(name="SupersetClient") + self.logger.info("[INFO][SupersetClient.__init__][ENTER] Initializing SupersetClient.") self._validate_config(config) self.config = config - - - # [ANCHOR] API_CLIENT_INIT - # [REFACTORING_COMPLETE] Теперь вся сетевая логика инкапсулирована в APIClient. - # APIClient отвечает за аутентификацию, повторные попытки и обработку низкоуровневых ошибок. self.network = APIClient( - base_url=config.base_url, - auth=config.auth, + config=config.dict(), verify_ssl=config.verify_ssl, timeout=config.timeout, - logger=self.logger # Передаем логгер в APIClient + logger=self.logger ) - - try: - # Аутентификация выполняется в конструкторе APIClient или по первому запросу - # Для явного вызова: self.network.authenticate() - # APIClient сам управляет токенами после первого успешного входа - self.logger.info( - "[COHERENCE_CHECK_PASSED] Клиент Superset успешно инициализирован", - extra={"base_url": config.base_url} - ) - except Exception as e: - self.logger.error( - "[INIT_FAILED] Ошибка инициализации клиента Superset", - exc_info=True, - extra={"config_base_url": config.base_url, "error": str(e)} - ) - raise # Перевыброс ошибки инициализации + self.logger.info("[INFO][SupersetClient.__init__][SUCCESS] SupersetClient initialized successfully.") + # END_FUNCTION___init__ + # [ENTITY: Function('_validate_config')] + # CONTRACT: + # PURPOSE: Валидация конфигурации клиента. + # PRECONDITIONS: `config` должен быть экземпляром `SupersetConfig`. + # POSTCONDITIONS: Конфигурация валидна. def _validate_config(self, config: SupersetConfig) -> None: - """[PRECONDITION] Валидация конфигурации клиента. - @semantic: - - Проверяет, что `config` является экземпляром `SupersetConfig`. - - Проверяет обязательные поля `base_url` и `auth`. - - Логирует ошибки валидации. - @raise: - - `TypeError`: если `config` не является `SupersetConfig`. - - `ValueError`: если отсутствуют обязательные поля или они невалидны. - """ + self.logger.debug("[DEBUG][SupersetClient._validate_config][ENTER] Validating config.") if not isinstance(config, SupersetConfig): - self.logger.error( - "[CONTRACT_VIOLATION] Некорректный тип конфигурации", - extra={"actual_type": type(config).__name__} - ) + self.logger.error("[ERROR][SupersetClient._validate_config][FAILURE] Invalid config type.") raise TypeError("Конфигурация должна быть экземпляром SupersetConfig") + self.logger.debug("[DEBUG][SupersetClient._validate_config][SUCCESS] Config validated.") + # END_FUNCTION__validate_config - # Pydantic SupersetConfig уже выполняет основную валидацию через Field и validator. - # Здесь можно добавить дополнительные бизнес-правила или проверки доступности, если нужно. - try: - # Попытка доступа к полям через Pydantic для проверки их существования - _ = config.base_url - _ = config.auth - _ = config.auth.get("username") - _ = config.auth.get("password") - self.logger.debug("[COHERENCE_CHECK_PASSED] Конфигурация SupersetClient прошла внутреннюю валидацию.") - except Exception as e: - self.logger.error( - f"[CONTRACT_VIOLATION] Ошибка валидации полей конфигурации: {e}", - extra={"config_dict": config.dict()} - ) - raise ValueError(f"Конфигурация SupersetConfig невалидна: {e}") from e - @property def headers(self) -> dict: - """[INTERFACE] Базовые заголовки для API-вызовов. - @semantic: Делегирует получение актуальных заголовков `APIClient`. - @post: Всегда возвращает актуальные токены и CSRF-токен. - @invariant: Заголовки содержат 'Authorization' и 'X-CSRFToken'. - """ - # [REFACTORING_COMPLETE] Заголовки теперь управляются APIClient. + """[INTERFACE] Базовые заголовки для API-вызовов.""" return self.network.headers + # END_FUNCTION_headers - # [SECTION] API для получения списка дашбордов или получения одного дашборда + # [ENTITY: Function('get_dashboards')] + # CONTRACT: + # PURPOSE: Получение списка дашбордов с пагинацией. + # PRECONDITIONS: None + # POSTCONDITIONS: Возвращает кортеж с общим количеством и списком дашбордов. def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: - """[CONTRACT] Получение списка дашбордов с пагинацией. - @pre: - - Клиент должен быть авторизован. - - Параметры `query` (если предоставлены) должны быть валидны для API Superset. - @post: - - Возвращает кортеж: (общее_количество_дашбордов, список_метаданных_дашбордов). - - Обходит пагинацию для получения всех доступных дашбордов. - @invariant: - - Всегда возвращает полный список (если `total_count` > 0). - @raise: - - `SupersetAPIError`: При ошибках API (например, неверный формат ответа). - - `NetworkError`: При проблемах с сетью. - - `ValueError`: При некорректных параметрах пагинации (внутренняя ошибка). - """ - self.logger.info("[INFO] Запрос списка всех дашбордов.") - # [COHERENCE_CHECK] Валидация и нормализация параметров запроса + self.logger.info("[INFO][SupersetClient.get_dashboards][ENTER] Getting dashboards.") validated_query = self._validate_query_params(query) - self.logger.debug("[DEBUG] Параметры запроса списка дашбордов после валидации.", extra={"validated_query": validated_query}) - - try: - # [ANCHOR] FETCH_TOTAL_COUNT - total_count = self._fetch_total_object_count(endpoint="/dashboard/") - self.logger.info(f"[INFO] Обнаружено {total_count} дашбордов в системе.") - - # [ANCHOR] FETCH_ALL_PAGES - paginated_data = self._fetch_all_pages(endpoint="/dashboard/", - query=validated_query, - total_count=total_count) - - self.logger.info( - f"[COHERENCE_CHECK_PASSED] Успешно получено {len(paginated_data)} дашбордов из {total_count}." - ) - return total_count, paginated_data - - except (SupersetAPIError, NetworkError, ValueError, PermissionDeniedError) as e: - self.logger.error(f"[ERROR] Ошибка при получении списка дашбордов: {str(e)}", exc_info=True, extra=getattr(e, 'context', {})) - raise - except Exception as e: - error_ctx = {"query": query, "error_type": type(e).__name__} - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при получении списка дашбордов: {str(e)}", exc_info=True, extra=error_ctx) - raise SupersetAPIError(f"Непредвиденная ошибка: {str(e)}", context=error_ctx) from e - - def get_dashboard(self, dashboard_id_or_slug: str) -> dict: - """[CONTRACT] Получение метаданных дашборда по ID или SLUG. - @pre: - - `dashboard_id_or_slug` должен быть строкой (ID или slug). - - Клиент должен быть аутентифицирован (токены актуальны). - @post: - - Возвращает `dict` с метаданными дашборда. - @raise: - - `DashboardNotFoundError`: Если дашборд не найден (HTTP 404). - - `SupersetAPIError`: При других ошибках API. - - `NetworkError`: При проблемах с сетью. - """ - self.logger.info(f"[INFO] Запрос метаданных дашборда: {dashboard_id_or_slug}") - try: - response_data = self.network.request( - method="GET", - endpoint=f"/dashboard/{dashboard_id_or_slug}", - # headers=self.headers # [REFACTORING_NOTE] APIClient теперь сам добавляет заголовки - ) - # [POSTCONDITION] Проверка структуры ответа - if "result" not in response_data: - self.logger.warning("[CONTRACT_VIOLATION] Ответ API не содержит поле 'result'", extra={"response": response_data}) - raise SupersetAPIError("Некорректный формат ответа API при получении дашборда") - self.logger.debug(f"[DEBUG] Метаданные дашборда '{dashboard_id_or_slug}' успешно получены.") - return response_data["result"] - except (DashboardNotFoundError, SupersetAPIError, NetworkError, PermissionDeniedError) as e: - self.logger.error(f"[ERROR] Не удалось получить дашборд '{dashboard_id_or_slug}': {str(e)}", exc_info=True, extra=getattr(e, 'context', {})) - raise # Перевыброс уже типизированной ошибки - except Exception as e: - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при получении дашборда '{dashboard_id_or_slug}': {str(e)}", exc_info=True) - raise SupersetAPIError(f"Непредвиденная ошибка: {str(e)}", context={"dashboard_id_or_slug": dashboard_id_or_slug}) from e - - # [SECTION] API для получения списка датасетов или получения одного датасета - def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: - """[CONTRACT] Получение списка датасетов с пагинацией. - @pre: - - Клиент должен быть авторизован. - - Параметры `query` (если предоставлены) должны быть валидны для API Superset. - @post: - - Возвращает кортеж: (общее_количество_датасетов, список_метаданных_датасетов). - - Обходит пагинацию для получения всех доступных датасетов. - @invariant: - - Всегда возвращает полный список (если `total_count` > 0). - @raise: - - `SupersetAPIError`: При ошибках API (например, неверный формат ответа). - - `NetworkError`: При проблемах с сетью. - - `ValueError`: При некорректных параметрах пагинации (внутренняя ошибка). - """ - self.logger.info("[INFO] Запрос списка всех датасетов") - - try: - # Получаем общее количество датасетов - total_count = self._fetch_total_object_count(endpoint="/dataset/") - self.logger.info(f"[INFO] Обнаружено {total_count} датасетов в системе") - - # Валидируем параметры запроса - base_query = { - "columns": ["id", "table_name", "sql", "database", "schema"], - "page": 0, - "page_size": 100 + 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", } - validated_query = {**base_query, **(query or {})} + ) + self.logger.info("[INFO][SupersetClient.get_dashboards][SUCCESS] Got dashboards.") + return total_count, paginated_data + # END_FUNCTION_get_dashboards - # Получаем все страницы - datasets = self._fetch_all_pages( - endpoint="/dataset/", - query=validated_query, - total_count=total_count#, - #results_field="result" - ) + # [ENTITY: Function('get_dashboard')] + # CONTRACT: + # PURPOSE: Получение метаданных дашборда по ID или SLUG. + # PRECONDITIONS: `dashboard_id_or_slug` должен существовать. + # POSTCONDITIONS: Возвращает метаданные дашборда. + def get_dashboard(self, dashboard_id_or_slug: str) -> dict: + self.logger.info(f"[INFO][SupersetClient.get_dashboard][ENTER] Getting dashboard: {dashboard_id_or_slug}") + response_data = self.network.request( + method="GET", + endpoint=f"/dashboard/{dashboard_id_or_slug}", + ) + self.logger.info(f"[INFO][SupersetClient.get_dashboard][SUCCESS] Got dashboard: {dashboard_id_or_slug}") + return response_data.get("result", {}) + # END_FUNCTION_get_dashboard - self.logger.info( - f"[COHERENCE_CHECK_PASSED] Успешно получено {len(datasets)} датасетов" - ) - return total_count, datasets - - except Exception as e: - error_ctx = {"query": query, "error_type": type(e).__name__} - self.logger.error( - f"[ERROR] Ошибка получения списка датасетов: {str(e)}", - exc_info=True, - extra=error_ctx - ) - raise - + # [ENTITY: Function('get_datasets')] + # CONTRACT: + # PURPOSE: Получение списка датасетов с пагинацией. + # PRECONDITIONS: None + # POSTCONDITIONS: Возвращает кортеж с общим количеством и списком датасетов. + def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: + self.logger.info("[INFO][SupersetClient.get_datasets][ENTER] Getting datasets.") + total_count = self._fetch_total_object_count(endpoint="/dataset/") + base_query = { + "columns": ["id", "table_name", "sql", "database", "schema"], + "page": 0, + "page_size": 100 + } + validated_query = {**base_query, **(query or {})} + datasets = self._fetch_all_pages( + endpoint="/dataset/", + pagination_options={ + "base_query": validated_query, + "total_count": total_count, + "results_field": "result", + } + ) + self.logger.info("[INFO][SupersetClient.get_datasets][SUCCESS] Got datasets.") + return total_count, datasets + # END_FUNCTION_get_datasets + + # [ENTITY: Function('get_dataset')] + # CONTRACT: + # PURPOSE: Получение метаданных датасета по ID. + # PRECONDITIONS: `dataset_id` должен существовать. + # POSTCONDITIONS: Возвращает метаданные датасета. def get_dataset(self, dataset_id: str) -> dict: - """[CONTRACT] Получение метаданных датасета по ID. - @pre: - - `dataset_id` должен быть строкой (ID или slug). - - Клиент должен быть аутентифицирован (токены актуальны). - @post: - - Возвращает `dict` с метаданными датасета. - @raise: - - `DashboardNotFoundError`: Если дашборд не найден (HTTP 404). - - `SupersetAPIError`: При других ошибках API. - - `NetworkError`: При проблемах с сетью. - """ - self.logger.info(f"[INFO] Запрос метаданных дашборда: {dataset_id}") - try: - response_data = self.network.request( - method="GET", - endpoint=f"/dataset/{dataset_id}", - # headers=self.headers # [REFACTORING_NOTE] APIClient теперь сам добавляет заголовки - ) - # [POSTCONDITION] Проверка структуры ответа - if "result" not in response_data: - self.logger.warning("[CONTRACT_VIOLATION] Ответ API не содержит поле 'result'", extra={"response": response_data}) - raise SupersetAPIError("Некорректный формат ответа API при получении дашборда") - self.logger.debug(f"[DEBUG] Метаданные дашборда '{dataset_id}' успешно получены.") - return response_data["result"] - except (DashboardNotFoundError, SupersetAPIError, NetworkError, PermissionDeniedError) as e: - self.logger.error(f"[ERROR] Не удалось получить дашборд '{dataset_id}': {str(e)}", exc_info=True, extra=getattr(e, 'context', {})) - raise # Перевыброс уже типизированной ошибки - except Exception as e: - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при получении дашборда '{dataset_id}': {str(e)}", exc_info=True) - raise SupersetAPIError(f"Непредвиденная ошибка: {str(e)}", context={"dashboard_id_or_slug": dataset_id}) from e + self.logger.info(f"[INFO][SupersetClient.get_dataset][ENTER] Getting dataset: {dataset_id}") + response_data = self.network.request( + method="GET", + endpoint=f"/dataset/{dataset_id}", + ) + self.logger.info(f"[INFO][SupersetClient.get_dataset][SUCCESS] Got dataset: {dataset_id}") + return response_data.get("result", {}) + # END_FUNCTION_get_dataset - # [SECTION] EXPORT OPERATIONS + # [ENTITY: Function('export_dashboard')] + # CONTRACT: + # PURPOSE: Экспорт дашборда в ZIP-архив. + # PRECONDITIONS: `dashboard_id` должен существовать. + # POSTCONDITIONS: Возвращает содержимое ZIP-архива и имя файла. def export_dashboard(self, dashboard_id: int) -> Tuple[bytes, str]: - """[CONTRACT] Экспорт дашборда в ZIP-архив. - @pre: - - `dashboard_id` должен быть целочисленным ID существующего дашборда. - - Пользователь должен иметь права на экспорт. - @post: - - Возвращает кортеж: (бинарное_содержимое_zip, имя_файла). - - Имя файла извлекается из заголовков `Content-Disposition` или генерируется. - @raise: - - `DashboardNotFoundError`: Если дашборд с `dashboard_id` не найден (HTTP 404). - - `ExportError`: При любых других проблемах экспорта (например, неверный тип контента, пустой ответ). - - `NetworkError`: При проблемах с сетью. - """ - self.logger.info(f"[INFO] Запуск экспорта дашборда с ID: {dashboard_id}") - try: - # [ANCHOR] EXECUTE_EXPORT_REQUEST - # [REFACTORING_COMPLETE] Использование self.network.request для экспорта - response = self.network.request( - method="GET", - endpoint="/dashboard/export/", - params={"q": json.dumps([dashboard_id])}, - stream=True, # Используем stream для обработки больших файлов - raw_response=True # Получаем сырой объект ответа requests.Response - # headers=self.headers # APIClient сам добавляет заголовки - ) - response.raise_for_status() # Проверка статуса ответа - - # [ANCHOR] VALIDATE_EXPORT_RESPONSE - self._validate_export_response(response, dashboard_id) - - # [ANCHOR] RESOLVE_FILENAME - filename = self._resolve_export_filename(response, dashboard_id) - - # [POSTCONDITION] Успешный экспорт - content = response.content # Получаем все содержимое - self.logger.info( - f"[COHERENCE_CHECK_PASSED] Дашборд {dashboard_id} успешно экспортирован. Размер: {len(content)} байт, Имя файла: {filename}" - ) - return content, filename - - except (DashboardNotFoundError, ExportError, NetworkError, PermissionDeniedError, SupersetAPIError) as e: - # Перехват и перевыброс уже типизированных ошибок от APIClient или предыдущих валидаций - self.logger.error(f"[ERROR] Ошибка экспорта дашборда {dashboard_id}: {str(e)}", exc_info=True, extra=getattr(e, 'context', {})) - raise - except Exception as e: - # Обработка любых непредвиденных ошибок - error_ctx = {"dashboard_id": dashboard_id, "error_type": type(e).__name__} - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при экспорте дашборда {dashboard_id}: {str(e)}", exc_info=True, extra=error_ctx) - raise ExportError(f"Непредвиденная ошибка при экспорте: {str(e)}", context=error_ctx) from e - - # [HELPER] Метод _execute_export_request был инлайнирован в export_dashboard - # Это сделано, чтобы избежать лишней абстракции, так как он просто вызывает self.network.request. - # Валидация HTTP-ответа и ошибок теперь происходит в self.network.request и последующей self.raise_for_status(). + self.logger.info(f"[INFO][SupersetClient.export_dashboard][ENTER] Exporting dashboard: {dashboard_id}") + response = self.network.request( + method="GET", + endpoint="/dashboard/export/", + params={"q": json.dumps([dashboard_id])}, + stream=True, + raw_response=True + ) + self._validate_export_response(response, dashboard_id) + filename = self._resolve_export_filename(response, dashboard_id) + content = response.content + self.logger.info(f"[INFO][SupersetClient.export_dashboard][SUCCESS] Exported dashboard: {dashboard_id}") + return content, filename + # END_FUNCTION_export_dashboard + # [ENTITY: Function('_validate_export_response')] + # CONTRACT: + # PURPOSE: Валидация ответа экспорта. + # PRECONDITIONS: `response` должен быть валидным HTTP-ответом. + # POSTCONDITIONS: Ответ валиден. def _validate_export_response(self, response: Response, dashboard_id: int) -> None: - """[HELPER] Валидация ответа экспорта. - @semantic: - - Проверяет, что Content-Type является `application/zip`. - - Проверяет, что ответ не пуст. - @raise: - - `ExportError`: При невалидном Content-Type или пустом содержимом. - """ + self.logger.debug(f"[DEBUG][SupersetClient._validate_export_response][ENTER] Validating export response for dashboard: {dashboard_id}") content_type = response.headers.get('Content-Type', '') if 'application/zip' not in content_type: - self.logger.error( - "[CONTRACT_VIOLATION] Неверный Content-Type для экспорта", - extra={ - "dashboard_id": dashboard_id, - "expected_type": "application/zip", - "received_type": content_type - } - ) + self.logger.error(f"[ERROR][SupersetClient._validate_export_response][FAILURE] Invalid content type: {content_type}") raise ExportError(f"Получен не ZIP-архив (Content-Type: {content_type})") - if not response.content: - self.logger.error( - "[CONTRACT_VIOLATION] Пустой ответ при экспорте дашборда", - extra={"dashboard_id": dashboard_id} - ) + self.logger.error("[ERROR][SupersetClient._validate_export_response][FAILURE] Empty response content.") raise ExportError("Получены пустые данные при экспорте") - self.logger.debug(f"[COHERENCE_CHECK_PASSED] Ответ экспорта для дашборда {dashboard_id} валиден.") + self.logger.debug(f"[DEBUG][SupersetClient._validate_export_response][SUCCESS] Export response validated for dashboard: {dashboard_id}") + # END_FUNCTION__validate_export_response + # [ENTITY: Function('_resolve_export_filename')] + # CONTRACT: + # PURPOSE: Определение имени экспортируемого файла. + # PRECONDITIONS: `response` должен быть валидным HTTP-ответом. + # POSTCONDITIONS: Возвращает имя файла. def _resolve_export_filename(self, response: Response, dashboard_id: int) -> str: - """[HELPER] Определение имени экспортируемого файла. - @semantic: - - Пытается извлечь имя файла из заголовка `Content-Disposition`. - - Если заголовок отсутствует, генерирует имя файла на основе ID дашборда и текущей даты. - @post: - - Возвращает строку с именем файла. - """ + self.logger.debug(f"[DEBUG][SupersetClient._resolve_export_filename][ENTER] Resolving export filename for dashboard: {dashboard_id}") filename = get_filename_from_headers(response.headers) if not filename: - # [FALLBACK] Генерация имени файла - filename = f"dashboard_export_{dashboard_id}_{datetime.datetime.now().strftime('%Y%m%dT%H%M%S')}.zip" - self.logger.warning( - "[WARN] Не удалось извлечь имя файла из заголовков. Используется сгенерированное имя.", - extra={"generated_filename": filename, "dashboard_id": dashboard_id} - ) - else: - self.logger.debug( - "[DEBUG] Имя файла экспорта получено из заголовков.", - extra={"header_filename": filename, "dashboard_id": dashboard_id} - ) + timestamp = datetime.datetime.now().strftime('%Y%m%dT%H%M%S') + filename = f"dashboard_export_{dashboard_id}_{timestamp}.zip" + self.logger.warning(f"[WARNING][SupersetClient._resolve_export_filename][STATE_CHANGE] Could not resolve filename from headers, generated: {filename}") + self.logger.debug(f"[DEBUG][SupersetClient._resolve_export_filename][SUCCESS] Resolved export filename: {filename}") return filename + # END_FUNCTION__resolve_export_filename + # [ENTITY: Function('export_to_file')] + # CONTRACT: + # PURPOSE: Экспорт дашборда напрямую в файл. + # PRECONDITIONS: `output_dir` должен существовать. + # POSTCONDITIONS: Дашборд сохранен в файл. def export_to_file(self, dashboard_id: int, output_dir: Union[str, Path]) -> Path: - """[CONTRACT] Экспорт дашборда напрямую в файл. - @pre: - - `dashboard_id` должен быть существующим ID дашборда. - - `output_dir` должен быть валидным, существующим путем и иметь права на запись. - @post: - - Дашборд экспортируется и сохраняется как ZIP-файл в `output_dir`. - - Возвращает `Path` к сохраненному файлу. - @raise: - - `FileNotFoundError`: Если `output_dir` не существует. - - `ExportError`: При ошибках экспорта или записи файла. - - `NetworkError`: При проблемах с сетью. - """ + self.logger.info(f"[INFO][SupersetClient.export_to_file][ENTER] Exporting dashboard {dashboard_id} to file in {output_dir}") output_dir = Path(output_dir) if not output_dir.exists(): - self.logger.error( - "[CONTRACT_VIOLATION] Целевая директория для экспорта не найдена.", - extra={"output_dir": str(output_dir)} - ) + self.logger.error(f"[ERROR][SupersetClient.export_to_file][FAILURE] Output directory does not exist: {output_dir}") raise FileNotFoundError(f"Директория {output_dir} не найдена") - - self.logger.info(f"[INFO] Экспорт дашборда {dashboard_id} в файл в директорию: {output_dir}") - try: - content, filename = self.export_dashboard(dashboard_id) - target_path = output_dir / filename + content, filename = self.export_dashboard(dashboard_id) + target_path = output_dir / filename + with open(target_path, 'wb') as f: + f.write(content) + self.logger.info(f"[INFO][SupersetClient.export_to_file][SUCCESS] Exported dashboard {dashboard_id} to {target_path}") + return target_path + # END_FUNCTION_export_to_file - with open(target_path, 'wb') as f: - f.write(content) - - self.logger.info( - "[COHERENCE_CHECK_PASSED] Дашборд успешно сохранен на диск.", - extra={ - "dashboard_id": dashboard_id, - "file_path": str(target_path), - "file_size": len(content) - } - ) - return target_path - - except (FileNotFoundError, ExportError, NetworkError, SupersetAPIError, DashboardNotFoundError) as e: - self.logger.error(f"[ERROR] Ошибка сохранения дашборда {dashboard_id} на диск: {str(e)}", exc_info=True, extra=getattr(e, 'context', {})) - raise - except IOError as io_err: - error_ctx = {"target_path": str(target_path), "dashboard_id": dashboard_id} - self.logger.critical(f"[CRITICAL] Ошибка записи файла для дашборда {dashboard_id}: {str(io_err)}", exc_info=True, extra=error_ctx) - raise ExportError("Ошибка сохранения файла на диск") from io_err - except Exception as e: - error_ctx = {"dashboard_id": dashboard_id, "error_type": type(e).__name__} - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при экспорте в файл: {str(e)}", exc_info=True, extra=error_ctx) - raise ExportError(f"Непредвиденная ошибка экспорта в файл: {str(e)}", context=error_ctx) from e - - - # [SECTION] Импорт дашбордов + # [ENTITY: Function('import_dashboard')] + # CONTRACT: + # PURPOSE: Импорт дашборда из ZIP-архива. + # PRECONDITIONS: `file_name` должен быть валидным ZIP-файлом. + # POSTCONDITIONS: Возвращает ответ API. def import_dashboard(self, file_name: Union[str, Path]) -> Dict: - """[CONTRACT] Импорт дашборда из ZIP-архива. - @pre: - - `file_name` должен указывать на существующий и валидный ZIP-файл Superset экспорта. - - Пользователь должен иметь права на импорт дашбордов. - @post: - - Дашборд импортируется (или обновляется, если `overwrite` включен). - - Возвращает `dict` с ответом API об импорте. - @raise: - - `FileNotFoundError`: Если файл не существует. - - `InvalidZipFormatError`: Если файл не является корректным ZIP-архивом Superset. - - `PermissionDeniedError`: Если у пользователя нет прав на импорт. - - `SupersetAPIError`: При других ошибках API импорта. - - `NetworkError`: При проблемах с сетью. - """ - self.logger.info(f"[INFO] Инициирован импорт дашборда из файла: {file_name}") - # [PRECONDITION] Валидация входного файла + self.logger.info(f"[INFO][SupersetClient.import_dashboard][ENTER] Importing dashboard from: {file_name}") self._validate_import_file(file_name) - - try: - # [ANCHOR] UPLOAD_FILE_TO_API - # [REFACTORING_COMPLETE] Использование self.network.upload_file - import_response = self.network.upload_file( - endpoint="/dashboard/import/", - file_obj=Path(file_name), # Pathlib объект, который APIClient может преобразовать в бинарный - file_name=Path(file_name).name, # Имя файла для FormData - form_field="formData", - extra_data={'overwrite': 'true'}, # Предполагаем, что всегда хотим перезаписывать - timeout=self.config.timeout * 2 # Удвоенный таймаут для загрузки больших файлов - # headers=self.headers # APIClient сам добавляет заголовки - ) - # [POSTCONDITION] Проверка успешного ответа импорта (Superset обычно возвращает JSON) - if not isinstance(import_response, dict) or "message" not in import_response: - self.logger.warning("[CONTRACT_VIOLATION] Неожиданный формат ответа при импорте", extra={"response": import_response}) - raise SupersetAPIError("Неожиданный формат ответа после импорта дашборда.") + import_response = self.network.upload_file( + endpoint="/dashboard/import/", + 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 + ) + self.logger.info(f"[INFO][SupersetClient.import_dashboard][SUCCESS] Imported dashboard from: {file_name}") + return import_response + # END_FUNCTION_import_dashboard - self.logger.info( - f"[COHERENCE_CHECK_PASSED] Дашборд из '{file_name}' успешно импортирован.", - extra={"api_message": import_response.get("message", "N/A"), "file": file_name} - ) - return import_response - - except (FileNotFoundError, InvalidZipFormatError, PermissionDeniedError, SupersetAPIError, NetworkError, DashboardNotFoundError) as e: - self.logger.error(f"[ERROR] Ошибка импорта дашборда из '{file_name}': {str(e)}", exc_info=True, extra=getattr(e, 'context', {})) - raise - except Exception as e: - error_ctx = {"file": file_name, "error_type": type(e).__name__} - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при импорте дашборда: {str(e)}", exc_info=True, extra=error_ctx) - raise SupersetAPIError(f"Непредвиденная ошибка импорта: {str(e)}", context=error_ctx) from e - - # [SECTION] Приватные методы-помощники + # [ENTITY: Function('_validate_query_params')] + # CONTRACT: + # PURPOSE: Нормализация и валидация параметров запроса. + # PRECONDITIONS: None + # POSTCONDITIONS: Возвращает валидный словарь параметров. def _validate_query_params(self, query: Optional[Dict]) -> Dict: - """[HELPER] Нормализация и валидация параметров запроса для списка дашбордов. - @semantic: - - Устанавливает значения по умолчанию для `columns`, `page`, `page_size`. - - Объединяет предоставленные `query` параметры с дефолтными. - @post: - - Возвращает словарь с полными и валидными параметрами запроса. - """ + self.logger.debug("[DEBUG][SupersetClient._validate_query_params][ENTER] Validating query params.") base_query = { "columns": ["slug", "id", "changed_on_utc", "dashboard_title", "published"], "page": 0, - "page_size": 1000 # Достаточно большой размер страницы для обхода пагинации + "page_size": 1000 } - # [COHERENCE_CHECK_PASSED] Параметры запроса сформированы корректно. - return {**base_query, **(query or {})} + validated_query = {**base_query, **(query or {})} + self.logger.debug(f"[DEBUG][SupersetClient._validate_query_params][SUCCESS] Validated query params: {validated_query}") + return validated_query + # END_FUNCTION__validate_query_params + # [ENTITY: Function('_fetch_total_object_count')] + # CONTRACT: + # PURPOSE: Получение общего количества объектов. + # PRECONDITIONS: `endpoint` должен быть валидным. + # POSTCONDITIONS: Возвращает общее количество объектов. def _fetch_total_object_count(self, endpoint:str) -> int: - """[CONTRACT][HELPER] Получение общего количества объектов (дашбордов, датасетов, чартов, баз данных) в системе. - @delegates: - - Сетевой запрос к `APIClient.fetch_paginated_count`. - @pre: - - Клиент должен быть авторизован. - @post: - - Возвращает целочисленное количество дашбордов. - @raise: - - `SupersetAPIError` или `NetworkError` при проблемах с API/сетью. - """ - query_params_for_count = { - 'columns': ['id'], - 'page': 0, - 'page_size': 1 - } - self.logger.debug("[DEBUG] Запрос общего количества дашбордов.") - try: - # [REFACTORING_COMPLETE] Использование self.network.fetch_paginated_count - count = self.network.fetch_paginated_count( - endpoint=endpoint, - query_params=query_params_for_count, - count_field="count" - ) - self.logger.debug(f"[COHERENCE_CHECK_PASSED] Получено общее количество дашбордов: {count}") - return count - except (SupersetAPIError, NetworkError, PermissionDeniedError) as e: - self.logger.error(f"[ERROR] Ошибка получения общего количества дашбордов: {str(e)}", exc_info=True, extra=getattr(e, 'context', {})) - raise # Перевыброс ошибки - except Exception as e: - error_ctx = {"error_type": type(e).__name__} - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при получении общего количества: {str(e)}", exc_info=True, extra=error_ctx) - raise SupersetAPIError(f"Непредвиденная ошибка при получении count: {str(e)}", context=error_ctx) from e + self.logger.debug(f"[DEBUG][SupersetClient._fetch_total_object_count][ENTER] Fetching total object count for endpoint: {endpoint}") + query_params_for_count = {'page': 0, 'page_size': 1} + count = self.network.fetch_paginated_count( + endpoint=endpoint, + query_params=query_params_for_count, + count_field="count" + ) + self.logger.debug(f"[DEBUG][SupersetClient._fetch_total_object_count][SUCCESS] Fetched total object count: {count}") + return count + # END_FUNCTION__fetch_total_object_count - def _fetch_all_pages(self, endpoint:str, query: Dict, total_count: int) -> List[Dict]: - """[CONTRACT][HELPER] Обход всех страниц пагинированного API для получения всех данных. - @delegates: - - Сетевые запросы к `APIClient.fetch_paginated_data()`. - @pre: - - `query` должен содержать `page_size`. - - `total_count` должен быть корректным общим количеством элементов. - - `endpoint` должен содержать часть url запроса, например endpoint="/dashboard/". - @post: - - Возвращает список всех элементов, собранных со всех страниц. - @raise: - - `SupersetAPIError` или `NetworkError` при проблемах с API/сетью. - - `ValueError` при некорректных параметрах пагинации. - """ - self.logger.debug(f"[DEBUG] Запуск обхода пагинации. Всего элементов: {total_count}, query: {query}") - try: - if 'page_size' not in query or not query['page_size']: - self.logger.error("[CONTRACT_VIOLATION] Параметр 'page_size' отсутствует или неверен в query.") - raise ValueError("Отсутствует 'page_size' в query параметрах для пагинации") - - # [REFACTORING_COMPLETE] Использование self.network.fetch_paginated_data - all_data = self.network.fetch_paginated_data( - endpoint=endpoint, - base_query=query, - total_count=total_count, - results_field="result" - ) - self.logger.debug(f"[COHERENCE_CHECK_PASSED] Успешно получено {len(all_data)} элементов со всех страниц.") - return all_data - - except (SupersetAPIError, NetworkError, ValueError, PermissionDeniedError) as e: - self.logger.error(f"[ERROR] Ошибка при обходе пагинации: {str(e)}", exc_info=True, extra=getattr(e, 'context', {})) - raise - except Exception as e: - error_ctx = {"query": query, "total_count": total_count, "error_type": type(e).__name__} - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при обходе пагинации: {str(e)}", exc_info=True, extra=error_ctx) - raise SupersetAPIError(f"Непредвиденная ошибка пагинации: {str(e)}", context=error_ctx) from e + # [ENTITY: Function('_fetch_all_pages')] + # CONTRACT: + # PURPOSE: Обход всех страниц пагинированного API. + # PRECONDITIONS: `pagination_options` должен содержать необходимые параметры. + # POSTCONDITIONS: Возвращает список всех объектов. + def _fetch_all_pages(self, endpoint:str, pagination_options: Dict) -> List[Dict]: + self.logger.debug(f"[DEBUG][SupersetClient._fetch_all_pages][ENTER] Fetching all pages for endpoint: {endpoint}") + all_data = self.network.fetch_paginated_data( + endpoint=endpoint, + pagination_options=pagination_options + ) + self.logger.debug(f"[DEBUG][SupersetClient._fetch_all_pages][SUCCESS] Fetched all pages for endpoint: {endpoint}") + return all_data + # END_FUNCTION__fetch_all_pages + # [ENTITY: Function('_validate_import_file')] + # CONTRACT: + # PURPOSE: Проверка файла перед импортом. + # PRECONDITIONS: `zip_path` должен быть путем к файлу. + # POSTCONDITIONS: Файл валиден. def _validate_import_file(self, zip_path: Union[str, Path]) -> None: - """[HELPER] Проверка файла перед импортом. - @semantic: - - Проверяет существование файла. - - Проверяет, что файл является валидным ZIP-архивом. - - Проверяет, что ZIP-архив содержит `metadata.yaml` (ключевой для экспорта Superset). - @raise: - - `FileNotFoundError`: Если файл не существует. - - `InvalidZipFormatError`: Если файл не ZIP или не содержит `metadata.yaml`. - """ + self.logger.debug(f"[DEBUG][SupersetClient._validate_import_file][ENTER] Validating import file: {zip_path}") path = Path(zip_path) - self.logger.debug(f"[DEBUG] Валидация файла для импорта: {path}") - if not path.exists(): - self.logger.error( - "[CONTRACT_VIOLATION] Файл для импорта не найден.", - extra={"file_path": str(path)} - ) + self.logger.error(f"[ERROR][SupersetClient._validate_import_file][FAILURE] Import file does not exist: {zip_path}") raise FileNotFoundError(f"Файл {zip_path} не существует") - if not zipfile.is_zipfile(path): - self.logger.error( - "[CONTRACT_VIOLATION] Файл не является валидным ZIP-архивом.", - extra={"file_path": str(path)} - ) + self.logger.error(f"[ERROR][SupersetClient._validate_import_file][FAILURE] Import file is not a zip file: {zip_path}") raise InvalidZipFormatError(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(f"[ERROR][SupersetClient._validate_import_file][FAILURE] Import file does not contain metadata.yaml: {zip_path}") + raise InvalidZipFormatError(f"Архив {zip_path} не содержит 'metadata.yaml'") + self.logger.debug(f"[DEBUG][SupersetClient._validate_import_file][SUCCESS] Validated import file: {zip_path}") + # END_FUNCTION__validate_import_file - try: - with zipfile.ZipFile(path, 'r') as zf: - # [CONTRACT] Проверяем наличие metadata.yaml - if not any(n.endswith('metadata.yaml') for n in zf.namelist()): - self.logger.error( - "[CONTRACT_VIOLATION] ZIP-архив не содержит 'metadata.yaml'.", - extra={"file_path": str(path), "zip_contents": zf.namelist()[:5]} # Логируем первые 5 файлов для отладки - ) - raise InvalidZipFormatError(f"Архив {zip_path} не содержит 'metadata.yaml', не является корректным экспортом Superset.") - self.logger.debug(f"[COHERENCE_CHECK_PASSED] Файл '{path}' успешно прошел валидацию для импорта.") - except zipfile.BadZipFile as e: - self.logger.error( - f"[CONTRACT_VIOLATION] Ошибка чтения ZIP-файла: {str(e)}", - exc_info=True, extra={"file_path": str(path)} - ) - raise InvalidZipFormatError(f"Файл {zip_path} поврежден или имеет некорректный формат ZIP.") from e - except Exception as e: - self.logger.critical( - f"[CRITICAL] Непредвиденная ошибка при валидации ZIP-файла: {str(e)}", - exc_info=True, extra={"file_path": str(path)} - ) - raise SupersetAPIError(f"Непредвиденная ошибка валидации ZIP: {str(e)}", context={"file_path": str(path)}) from e \ No newline at end of file diff --git a/superset_tool/exceptions.py b/superset_tool/exceptions.py index 80a9ecb..e371190 100644 --- a/superset_tool/exceptions.py +++ b/superset_tool/exceptions.py @@ -1,153 +1,124 @@ -# [MODULE] Иерархия исключений -# @contract: Все ошибки наследуют `SupersetToolError` для единой точки обработки. -# @semantic: Каждый тип исключения соответствует конкретной проблемной области в инструменте Superset. -# @coherence: -# - Полное покрытие всех сценариев ошибок клиента и утилит. -# - Четкая классификация по уровню серьезности (от общей до специфичной). -# - Дополнительный `context` для каждой ошибки, помогающий в диагностике. +# pylint: disable=too-many-ancestors +""" +[MODULE] Иерархия исключений +@contract: Все ошибки наследуют `SupersetToolError` для единой точки обработки. +""" # [IMPORTS] Standard library from pathlib import Path # [IMPORTS] Typing -from typing import Optional, Dict, Any,Union +from typing import Optional, Dict, Any, Union class SupersetToolError(Exception): - """[BASE] Базовый класс для всех ошибок инструмента Superset. - @semantic: Обеспечивает стандартизированный формат сообщений об ошибках с контекстом. - @invariant: - - `message` всегда присутствует. - - `context` всегда является словарем, даже если пустой. - """ + """[BASE] Базовый класс для всех ошибок инструмента Superset.""" + # [ENTITY: Function('__init__')] + # CONTRACT: + # PURPOSE: Инициализация базового исключения. + # PRECONDITIONS: `context` должен быть словарем или None. + # POSTCONDITIONS: Исключение создано с сообщением и контекстом. def __init__(self, message: str, context: Optional[Dict[str, Any]] = None): - # [PRECONDITION] Проверка типа контекста if not isinstance(context, (dict, type(None))): - # [COHERENCE_CHECK_FAILED] Ошибка в передаче контекста raise TypeError("Контекст ошибки должен быть словарем или None") self.context = context or {} super().__init__(f"{message} | Context: {self.context}") - # [POSTCONDITION] Логирование создания ошибки - # Можно добавить здесь логирование, но обычно ошибки логируются в месте их перехвата/подъема, - # чтобы избежать дублирования и получить полный стек вызовов. + # END_FUNCTION___init__ -# [ERROR-GROUP] Проблемы аутентификации и авторизации class AuthenticationError(SupersetToolError): - """[AUTH] Ошибки аутентификации (неверные учетные данные) или авторизации (проблемы с сессией). - @context: url, username, error_detail (опционально). - """ - # [CONTRACT] - # Description: Исключение, возникающее при ошибках аутентификации в Superset API. + """[AUTH] Ошибки аутентификации или авторизации.""" + # [ENTITY: Function('__init__')] + # CONTRACT: + # PURPOSE: Инициализация исключения аутентификации. + # PRECONDITIONS: None + # POSTCONDITIONS: Исключение создано. def __init__(self, message: str = "Authentication failed", **context: Any): - super().__init__( - f"[AUTH_FAILURE] {message}", - {"type": "authentication", **context} - ) + super().__init__(f"[AUTH_FAILURE] {message}", context={"type": "authentication", **context}) + # END_FUNCTION___init__ class PermissionDeniedError(AuthenticationError): - """[AUTH] Ошибка отказа в доступе из-за недостаточных прав пользователя. - @semantic: Указывает на то, что операция не разрешена. - @context: required_permission (опционально), user_roles (опционально), endpoint (опционально). - @invariant: Наследует от `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, - {"type": "authorization", "required_permission": required_permission, **context} - ) + super().__init__(full_message, context={"required_permission": required_permission, **context}) + # END_FUNCTION___init__ -# [ERROR-GROUP] Проблемы API-вызовов class SupersetAPIError(SupersetToolError): - """[API] Общие ошибки взаимодействия с Superset API. - @semantic: Для ошибок, возвращаемых Superset API, или проблем с парсингом ответа. - @context: endpoint, method, status_code, response_body (опционально), error_message (из API). - """ - # [CONTRACT] - # Description: Исключение, возникающее при получении ошибки от Superset API (статус код >= 400). + """[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}", - {"type": "api_call", **context} - ) + super().__init__(f"[API_FAILURE] {message}", context={"type": "api_call", **context}) + # END_FUNCTION___init__ -# [ERROR-SUBCLASS] Детализированные ошибки API class ExportError(SupersetAPIError): - """[API:EXPORT] Проблемы, специфичные для операций экспорта дашбордов. - @semantic: Может быть вызвано невалидным форматом ответа, ошибками Superset при экспорте. - @context: dashboard_id (опционально), details (опционально). - """ + """[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}", {"subtype": "export", **context}) + super().__init__(f"[EXPORT_FAILURE] {message}", context={"subtype": "export", **context}) + # END_FUNCTION___init__ class DashboardNotFoundError(SupersetAPIError): - """[API:404] Запрошенный дашборд или ресурс не существует. - @semantic: Соответствует HTTP 404 Not Found. - @context: dashboard_id_or_slug, url. - """ - # [CONTRACT] - # Description: Исключение, специфичное для случая, когда дашборд не найден (статус 404). + """[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}", - {"subtype": "not_found", "resource_id": dashboard_id_or_slug, **context} - ) - + 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__ + class DatasetNotFoundError(SupersetAPIError): - """[API:404] Запрашиваемый набор данных не существует. - @semantic: Соответствует HTTP 404 Not Found. - @context: dataset_id_or_slug, url. - """ - # [CONTRACT] - # Description: Исключение, специфичное для случая, когда набор данных не найден (статус 404). + """[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}", - {"subtype": "not_found", "resource_id": dataset_id_or_slug, **context} - ) + 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__ -# [ERROR-SUBCLASS] Детализированные ошибки обработки файлов class InvalidZipFormatError(SupersetToolError): - """[FILE:ZIP] Некорректный формат ZIP-архива или содержимого для импорта/экспорта. - @semantic: Указывает на проблемы с целостностью или структурой ZIP-файла. - @context: file_path, expected_content (например, metadata.yaml), error_detail. - """ + """[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}", - {"type": "file_validation", "file_path": str(file_path) if file_path else "N/A", **context} - ) + 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__ -# [ERROR-GROUP] Системные и network-ошибки class NetworkError(SupersetToolError): - """[NETWORK] Проблемы соединения, таймауты, DNS-ошибки и т.п. - @semantic: Ошибки, связанные с невозможностью установить или поддерживать сетевое соединение. - @context: url, original_exception (опционально), timeout (опционально). - """ - # [CONTRACT] - # Description: Исключение, возникающее при сетевых ошибках во время взаимодействия с Superset API. + """[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}", - {"type": "network", **context} - ) + super().__init__(f"[NETWORK_FAILURE] {message}", context={"type": "network", **context}) + # END_FUNCTION___init__ class FileOperationError(SupersetToolError): - """ - # [CONTRACT] - # Description: Исключение, возникающее при ошибках файловых операций (чтение, запись, архивирование). - """ - pass + """[FILE] Ошибка файловых операций.""" class InvalidFileStructureError(FileOperationError): - """ - # [CONTRACT] - # Description: Исключение, возникающее при обнаружении некорректной структуры файлов/директорий. - """ - pass + """[FILE] Некорректная структура файлов/директорий.""" class ConfigurationError(SupersetToolError): - """ - # [CONTRACT] - # Description: Исключение, возникающее при ошибках в конфигурации инструмента. - """ - pass + """[CONFIG] Ошибка в конфигурации инструмента.""" + diff --git a/superset_tool/models.py b/superset_tool/models.py index 354e664..55a11d7 100644 --- a/superset_tool/models.py +++ b/superset_tool/models.py @@ -1,28 +1,19 @@ -# [MODULE] Сущности данных конфигурации -# @desc: Определяет структуры данных, используемые для конфигурации и трансформации в инструменте Superset. -# @contracts: -# - Все модели наследуются от `pydantic.BaseModel` для автоматической валидации. -# - Валидация URL-адресов и параметров аутентификации. -# - Валидация структуры конфигурации БД для миграций. -# @coherence: -# - Все модели согласованы со схемой API Superset v1. -# - Совместимы с клиентскими методами `SupersetClient` и утилитами. +# pylint: disable=no-self-argument,too-few-public-methods +""" +[MODULE] Сущности данных конфигурации +@desc: Определяет структуры данных, используемые для конфигурации и трансформации в инструменте Superset. +""" # [IMPORTS] Pydantic и Typing -from typing import Optional, Dict, Any, Union -from pydantic import BaseModel, validator, Field, HttpUrl -# [COHERENCE_CHECK_PASSED] Все необходимые импорты для Pydantic моделей. +from typing import Optional, Dict, Any +from pydantic import BaseModel, validator, Field, HttpUrl, VERSION # [IMPORTS] Локальные модули from .utils.logger import SupersetLogger class SupersetConfig(BaseModel): - """[CONFIG] Конфигурация подключения к Superset API. - @semantic: Инкапсулирует основные параметры, необходимые для инициализации `SupersetClient`. - @invariant: - - `base_url` должен быть валидным HTTP(S) URL и содержать `/api/v1`. - - `auth` должен содержать обязательные поля для аутентификации по логину/паролю. - - `timeout` должен быть положительным числом. + """ + [CONFIG] Конфигурация подключения к Superset API. """ base_url: str = Field(..., description="Базовый URL Superset API, включая версию /api/v1.", pattern=r'.*/api/v1.*') auth: Dict[str, str] = Field(..., description="Словарь с данными для аутентификации (provider, username, password, refresh).") @@ -30,118 +21,69 @@ class SupersetConfig(BaseModel): timeout: int = Field(30, description="Таймаут в секундах для HTTP-запросов.") logger: Optional[SupersetLogger] = Field(None, description="Экземпляр логгера для логирования внутри клиента.") - # [VALIDATOR] Проверка параметров аутентификации + # [ENTITY: Function('validate_auth')] + # CONTRACT: + # PURPOSE: Валидация словаря `auth`. + # PRECONDITIONS: `v` должен быть словарем. + # POSTCONDITIONS: Возвращает `v` если все обязательные поля присутствуют. @validator('auth') - def validate_auth(cls, v: Dict[str, str]) -> Dict[str, str]: - """[CONTRACT_VALIDATOR] Валидация словаря `auth`. - @pre: - - `v` должен быть словарем. - @post: - - Возвращает `v` если все обязательные поля присутствуют. - @raise: - - `ValueError`: Если отсутствуют обязательные поля ('provider', 'username', 'password', 'refresh'). - """ + 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.") required = {'provider', 'username', 'password', 'refresh'} if not required.issubset(v.keys()): - raise ValueError( - f"[CONTRACT_VIOLATION] Словарь 'auth' должен содержать поля: {required}. " - f"Отсутствующие: {required - v.keys()}" - ) - # [COHERENCE_CHECK_PASSED] Auth-конфигурация валидна. + 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 - - # [VALIDATOR] Проверка base_url + # END_FUNCTION_validate_auth + + # [ENTITY: Function('check_base_url_format')] + # CONTRACT: + # PURPOSE: Валидация формата `base_url`. + # PRECONDITIONS: `v` должна быть строкой. + # POSTCONDITIONS: Возвращает `v` если это валидный URL. @validator('base_url') - def check_base_url_format(cls, v: str) -> str: - """[CONTRACT_VALIDATOR] Валидация формата `base_url`. - @pre: - - `v` должна быть строкой. - @post: - - Возвращает `v` если это валидный URL. - @raise: - - `ValueError`: Если URL невалиден. - """ + def check_base_url_format(cls, v: str, values: dict) -> str: + logger = values.get('logger') or SupersetLogger(name="SupersetConfig") + logger.debug("[DEBUG][SupersetConfig.check_base_url_format][ENTER] Validating base_url.") try: - # Для Pydantic v2: - from pydantic import HttpUrl - HttpUrl(v, scheme="https") # Явное указание схемы - except ValueError: - # Для совместимости с Pydantic v1: - HttpUrl(v) + if VERSION.startswith('1'): + HttpUrl(v) + except (ValueError, TypeError) as exc: + logger.error("[ERROR][SupersetConfig.check_base_url_format][FAILURE] Invalid base_url format.") + raise ValueError(f"Invalid URL format: {v}") from exc + logger.debug("[DEBUG][SupersetConfig.check_base_url_format][SUCCESS] base_url validated.") return v + # END_FUNCTION_check_base_url_format class Config: - arbitrary_types_allowed = True # Разрешаем Pydantic обрабатывать произвольные типы (например, SupersetLogger) - json_schema_extra = { - "example": { - "base_url": "https://host/api/v1/", - "auth": { - "provider": "db", - "username": "user", - "password": "pass", - "refresh": True - }, - "verify_ssl": True, - "timeout": 60 - } - } + """Pydantic config""" + arbitrary_types_allowed = True - -# [SEMANTIC-TYPE] Конфигурация БД для миграций class DatabaseConfig(BaseModel): - """[CONFIG] Параметры трансформации баз данных при миграции дашбордов. - @semantic: Содержит `old` и `new` состояния конфигурации базы данных, - используемые для поиска и замены в YAML-файлах экспортированных дашбордов. - @invariant: - - `database_config` должен быть словарем с ключами 'old' и 'new'. - - Каждое из 'old' и 'new' должно быть словарем, содержащим метаданные БД Superset. + """ + [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'. @validator('database_config') - def validate_config(cls, v: Dict[str, Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: - """[CONTRACT_VALIDATOR] Валидация словаря `database_config`. - @pre: - - `v` должен быть словарем. - @post: - - Возвращает `v` если содержит ключи 'old' и 'new'. - @raise: - - `ValueError`: Если отсутствуют ключи 'old' или 'new'. - """ + 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.") if not {'old', 'new'}.issubset(v.keys()): - raise ValueError( - "[CONTRACT_VIOLATION] 'database_config' должен содержать ключи 'old' и 'new'." - ) - # Дополнительно можно добавить проверку структуры `old` и `new` на наличие `uuid`, `database_name` и т.д. - # Для простоты пока ограничимся наличием ключей 'old' и 'new'. - # [COHERENCE_CHECK_PASSED] Конфигурация базы данных для миграции валидна. + 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 - json_schema_extra = { - "example": { - "database_config": { - "old": - { - "database_name": "Prod Clickhouse", - "sqlalchemy_uri": "clickhousedb+connect://clicketl:XXXXXXXXXX@rgm-s-khclk.hq.root.ad:443/dm", - "uuid": "b9b67cb5-9874-4dc6-87bd-354fc33be6f9", - "database_uuid": "b9b67cb5-9874-4dc6-87bd-354fc33be6f9", - "allow_ctas": "false", - "allow_cvas": "false", - "allow_dml": "false" - }, - "new": { - "database_name": "Dev Clickhouse", - "sqlalchemy_uri": "clickhousedb+connect://dwhuser:XXXXXXXXXX@10.66.229.179:8123/dm", - "uuid": "e9fd8feb-cb77-4e82-bc1d-44768b8d2fc2", - "database_uuid": "e9fd8feb-cb77-4e82-bc1d-44768b8d2fc2", - "allow_ctas": "true", - "allow_cvas": "true", - "allow_dml": "true" - } - } - } - } \ No newline at end of file diff --git a/superset_tool/utils/fileio.py b/superset_tool/utils/fileio.py index 0e6d47a..73e10e1 100644 --- a/superset_tool/utils/fileio.py +++ b/superset_tool/utils/fileio.py @@ -1,42 +1,51 @@ -# [MODULE] File Operations Manager -# @desc: Управление файловыми операциями для дашбордов Superset -# @contracts: -# 1. Валидация ZIP-архивов -# 2. Работа с YAML-конфигами -# 3. Управление директориями -# @coherence: -# - Согласован с SupersetClient -# - Поддерживает все форматы экспорта Superset +# -*- coding: utf-8 -*- +# pylint: disable=too-many-arguments,too-many-locals,too-many-statements,too-many-branches,unused-argument +""" +[MODULE] File Operations Manager +@contract: Предоставляет набор утилит для управления файловыми операциями. +""" # [IMPORTS] Core import os import re import zipfile from pathlib import Path -from typing import Any, Optional, Tuple, Dict, List, Literal, Union, BinaryIO, LiteralString -from collections import defaultdict -from datetime import date -import glob -import filecmp +from typing import Any, Optional, Tuple, Dict, List, Union, LiteralString from contextlib import contextmanager +import tempfile +from datetime import date, datetime +import glob +import shutil +import zlib +from dataclasses import dataclass # [IMPORTS] Third-party import yaml -import shutil -import zlib -import tempfile -from datetime import datetime # [IMPORTS] Local -from ..models import DatabaseConfig -from ..exceptions import InvalidZipFormatError, DashboardNotFoundError -from ..utils.logger import SupersetLogger +from superset_tool.exceptions import InvalidZipFormatError +from superset_tool.utils.logger import SupersetLogger # [CONSTANTS] ALLOWED_FOLDERS = {'databases', 'datasets', 'charts', 'dashboards'} - -# [CONTRACT] Временные ресурсы +# 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`, затем выбрасывает его дальше. @contextmanager def create_temp_file( content: Optional[bytes] = None, @@ -44,331 +53,297 @@ def create_temp_file( mode: str = 'wb', logger: Optional[SupersetLogger] = None ) -> Path: - """[CONTEXT-MANAGER] Создание временного файла/директории - @pre: - - suffix должен быть допустимым расширением - - mode соответствует типу содержимого - @post: - - Возвращает Path созданного ресурса - - Гарантирует удаление временного файла при выходе - """ + """Создает временный файл или директорию с автоматической очисткой.""" logger = logger or SupersetLogger(name="fileio", console=False) + temp_resource_path = None + is_dir = suffix.startswith('.dir') try: - if suffix.startswith('.dir'): - with tempfile.TemporaryDirectory(suffix=suffix) as tmp_dir: - logger.debug(f"Создана временная директория: {tmp_dir}") - yield Path(tmp_dir) + 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 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"Создан временный файл: {tmp.name}") - yield Path(tmp.name) - except Exception as e: - logger.error(f"[TEMP_FILE_ERROR] Ошибка создания ресурса: {str(e)}", exc_info=True) + 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 finally: - if 'tmp' in locals() and Path(tmp.name).exists() and not suffix.startswith('.dir'): - Path(tmp.name).unlink(missing_ok=True) + 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, - exclude: Optional[List[str]] = None, logger: Optional[SupersetLogger] = None ) -> int: - """[CONTRACT] Рекурсивное удаление пустых директорий - @pre: - - root_dir должен существовать и быть директорией - - exclude не должен содержать некорректных символов - @post: - - Возвращает количество удаленных директорий - - Не удаляет директории из списка exclude - - Гарантирует рекурсивную обработку вложенных папок - """ + """Рекурсивно удаляет пустые директории.""" logger = logger or SupersetLogger(name="fileio", console=False) - logger.info(f"[DIR_CLEANUP] Старт очистки пустых директорий в {root_dir}") - - excluded = set(exclude or []) + logger.info(f"[STATE][DIR_CLEANUP] Запуск очистки пустых директорий в {root_dir}") + removed_count = 0 root_path = Path(root_dir) - # [VALIDATION] Проверка корневой директории - if not root_path.exists(): - logger.error(f"[DIR_ERROR] Директория не существует: {root_dir}") + if not root_path.is_dir(): + logger.error(f"[STATE][DIR_NOT_FOUND] Директория не существует или не является директорией: {root_dir}") return 0 - try: - # [PROCESSING] Рекурсивный обход снизу вверх - for current_dir, _, files in os.walk(root_path, topdown=False): - current_path = Path(current_dir) - - # [EXCLUSION] Пропуск исключенных директорий - if any(excluded_part in current_path.parts for excluded_part in excluded): - logger.debug(f"[DIR_SKIP] Пропущено по исключению: {current_dir}") - continue - - # [REMOVAL] Удаление пустых директорий - if not any(current_path.iterdir()): - try: - current_path.rmdir() - removed_count += 1 - logger.info(f"[DIR_REMOVED] Удалена пустая директория: {current_dir}") - except OSError as e: - logger.error(f"[DIR_ERROR] Ошибка удаления {current_dir}: {str(e)}") - - except Exception as e: - logger.error(f"[DIR_CLEANUP_ERROR] Критическая ошибка: {str(e)}", exc_info=True) - raise + for current_dir, _, _ in os.walk(root_path, topdown=False): + if not os.listdir(current_dir): + try: + os.rmdir(current_dir) + removed_count += 1 + logger.info(f"[STATE][DIR_REMOVED] Удалена пустая директория: {current_dir}") + except OSError as e: + logger.error(f"[STATE][DIR_REMOVE_FAILED] Ошибка удаления {current_dir}: {str(e)}") - logger.info(f"[DIR_RESULT] Удалено {removed_count} пустых директорий") + logger.info(f"[STATE][DIR_CLEANUP_DONE] Удалено {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]: - """[CONTRACT] Чтение сохраненного дашборда с диска - @pre: - - file_path должен существовать - - Файл должен быть доступен для чтения - @post: - - Возвращает (содержимое файла, имя файла) - - Сохраняет целостность данных - """ + """Читает сохраненный дашборд с диска.""" logger = logger or SupersetLogger(name="fileio", console=False) - - try: - path = Path(file_path) - if not path.exists(): - raise FileNotFoundError(f"[FILE_MISSING] Файл дашборда не найден: {file_path}") - - logger.info(f"[FILE_READ] Чтение дашборда с диска: {file_path}") - - with open(file_path, "rb") as f: - content = f.read() - - if not content: - logger.warning("[FILE_EMPTY] Файл существует, но пуст") - - return content, path.name - - except Exception as e: - logger.error(f"[FILE_READ_ERROR] Ошибка чтения: {str(e)}", exc_info=True) - raise + 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}") + content = path.read_bytes() + if not content: + logger.warning(f"[STATE][FILE_EMPTY] Файл {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. def calculate_crc32(file_path: Path) -> str: - """[HELPER] Calculates the CRC32 checksum of a file. - @pre: - - file_path must be a valid path to a file. - @post: - - Returns the CRC32 checksum as a hexadecimal string. - @raise: - - FileNotFoundError: If the file does not exist. - - Exception: For any other file I/O errors. - """ + """Вычисляет CRC32 контрольную сумму файла.""" try: with open(file_path, 'rb') as f: crc32_value = zlib.crc32(f.read()) - return hex(crc32_value)[2:].zfill(8) # Convert to hex string, remove "0x", and pad with zeros + return f"{crc32_value:08x}" except FileNotFoundError: - raise FileNotFoundError(f"File not found: {file_path}") - except Exception as e: - raise Exception(f"Error calculating CRC32 for {file_path}: {str(e)}") + raise + except IOError as e: + raise IOError(f"Ошибка вычисления CRC32 для {file_path}: {str(e)}") from e +# END_FUNCTION_calculate_crc32 +@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, - daily_retention: int = 7, - weekly_retention: int = 4, - monthly_retention: int = 12, + policy: RetentionPolicy, deduplicate: bool = False, logger: Optional[SupersetLogger] = None ) -> None: - # [CONTRACT] Управление архивом экспортированных дашбордов - # @pre: - # - output_dir должен существовать - # - Значения retention должны быть >= 0 - # @post: - # - Сохраняет файлы согласно политике хранения - # - Удаляет устаревшие архивы - # - Логирует все действия - # @raise: - # - ValueError: Если retention параметры некорректны - # - Exception: При любых других ошибках + """Управляет архивом экспортированных дашбордов.""" logger = logger or SupersetLogger(name="fileio", console=False) - logger.info(f"[ARCHIVE] Starting archive cleanup in {output_dir}. Deduplication: {deduplicate}") - # [DEBUG_ARCHIVE] Log input parameters - logger.debug(f"[DEBUG_ARCHIVE] archive_exports called with: output_dir={output_dir}, daily={daily_retention}, weekly={weekly_retention}, monthly={monthly_retention}, deduplicate={deduplicate}") + output_path = Path(output_dir) + if not output_path.is_dir(): + logger.warning(f"[WARN][ARCHIVE] Директория архива не найдена: {output_dir}") + return - # [VALIDATION] Проверка параметров - if not all(isinstance(x, int) and x >= 0 for x in [daily_retention, weekly_retention, monthly_retention]): - raise ValueError("[CONFIG_ERROR] Retention values must be positive integers.") + logger.info(f"[INFO][ARCHIVE] Запуск управления архивом в {output_dir}") - checksums = {} # Dictionary to store checksums and file paths - try: - export_dir = Path(output_dir) - - if not export_dir.exists(): - logger.error(f"[ARCHIVE_ERROR] Directory does not exist: {export_dir}") - raise FileNotFoundError(f"Directory not found: {export_dir}") - - # [PROCESSING] Сбор информации о файлах - files_with_dates = [] - zip_files_in_dir = list(export_dir.glob("*.zip")) - # [DEBUG_ARCHIVE] Log number of zip files found - logger.debug(f"[DEBUG_ARCHIVE] Found {len(zip_files_in_dir)} zip files in {export_dir}") - - for file in zip_files_in_dir: - # [DEBUG_ARCHIVE] Log file being processed - logger.debug(f"[DEBUG_ARCHIVE] Processing file: {file.name}") + # 1. Дедупликация + if deduplicate: + checksums = {} + duplicates_removed = 0 + for file_path in output_path.glob('*.zip'): try: - timestamp_str = file.stem.split('_')[-1].split('T')[0] - file_date = datetime.strptime(timestamp_str, "%Y%m%d").date() - logger.debug(f"[DATE_PARSE] Файл {file.name} добавлен к анализу очистки (массив files_with_dates)") - # [DEBUG_ARCHIVE] Log parsed date - logger.debug(f"[DEBUG_ARCHIVE] Parsed date for {file.name}: {file_date}") - except (ValueError, IndexError): - file_date = datetime.fromtimestamp(file.stat().st_mtime).date() - logger.warning(f"[DATE_PARSE] Using modification date for {file.name}") - # [DEBUG_ARCHIVE] Log parsed date (modification date) - logger.debug(f"[DEBUG_ARCHIVE] Parsed date for {file.name} (mod date): {file_date}") + 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}") - files_with_dates.append((file, file_date)) + if not files_with_dates: + logger.info("[INFO][RETENTION] Не найдено файлов для применения политики хранения.") + return - - # [DEDUPLICATION] - if deduplicate: - logger.info("Начало дедупликации на основе контрольных сумм.") - for file in files_with_dates: - file_path = file[0] - # [DEBUG_ARCHIVE] Log file being checked for deduplication - logger.debug(f"[DEBUG_ARCHIVE][DEDUPLICATION] Checking file: {file_path.name}") - try: - crc32_checksum = calculate_crc32(file_path) - if crc32_checksum in checksums: - # Duplicate found, delete the older file - logger.warning(f"[DEDUPLICATION] Duplicate found: {file_path}. Deleting.") - # [DEBUG_ARCHIVE][DEDUPLICATION] Log duplicate found and deletion attempt - logger.debug(f"[DEBUG_ARCHIVE][DEDUPLICATION] Duplicate found: {file_path.name}. Checksum: {crc32_checksum}. Attempting deletion.") - file_path.unlink() - else: - checksums[crc32_checksum] = file_path - # [DEBUG_ARCHIVE][DEDUPLICATION] Log file kept after deduplication check - logger.debug(f"[DEBUG_ARCHIVE][DEDUPLICATION] Keeping file: {file_path.name}. Checksum: {crc32_checksum}.") - except Exception as e: - logger.error(f"[DEDUPLICATION_ERROR] Error processing {file_path}: {str(e)}", exc_info=True) - - # [PROCESSING] Применение политик хранения - # [DEBUG_ARCHIVE] Log files before retention policy - logger.debug(f"[DEBUG_ARCHIVE] Files with dates before retention policy: {[f.name for f, d in files_with_dates]}") - keep_files = apply_retention_policy( - files_with_dates, - daily_retention, - weekly_retention, - monthly_retention, - logger - ) - # [DEBUG_ARCHIVE] Log files to keep after retention policy - logger.debug(f"[DEBUG_ARCHIVE] Files to keep after retention policy: {[f.name for f in keep_files]}") - - - # [CLEANUP] Удаление устаревших файлов - deleted_count = 0 - files_to_delete = [] - files_to_keep = [] + files_to_keep = apply_retention_policy(files_with_dates, policy, logger) - for file, file_date in files_with_dates: - # [DEBUG_ARCHIVE] Check file for deletion - should_keep = file in keep_files - logger.debug(f"[DEBUG_ARCHIVE] Checking file for deletion: {file.name} (date: {file_date}). Should keep: {should_keep}") - - if should_keep: - files_to_keep.append(file.name) - else: - files_to_delete.append(file.name) + files_deleted = 0 + for file_path, _ in files_with_dates: + if file_path not in files_to_keep: try: - # [DEBUG_ARCHIVE][FILE_REMOVED_ATTEMPT] Log deletion attempt - logger.info(f"[DEBUG_ARCHIVE][FILE_REMOVED_ATTEMPT] Attempting to delete archive: {file.name}") - file.unlink() - deleted_count += 1 - logger.info(f"[FILE_REMOVED] Deleted archive: {file.name}") + file_path.unlink() + logger.info(f"[INFO][RETENTION] Удален устаревший архив: {file_path}") + files_deleted += 1 except OSError as e: - # [DEBUG_ARCHIVE][FILE_ERROR] Log deletion error - logger.error(f"[DEBUG_ARCHIVE][FILE_ERROR] Error deleting {file.name}: {str(e)}", exc_info=True) + logger.error(f"[ERROR][RETENTION] Не удалось удалить файл {file_path}: {e}") - logger.debug(f"[DEBUG_ARCHIVE] Summary - Files to keep: {files_to_keep}") - logger.debug(f"[DEBUG_ARCHIVE] Summary - Files to delete: {files_to_delete}") - - - logger.info(f"[ARCHIVE_RESULT] Cleanup completed. Deleted {deleted_count} archives.") + logger.info(f"[INFO][RETENTION] Политика хранения применена. Удалено файлов: {files_deleted}.") except Exception as e: - logger.error(f"[ARCHIVE_ERROR] Critical error during archive cleanup: {str(e)}", exc_info=True) - raise + 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]], - daily: int, - weekly: int, - monthly: int, + policy: RetentionPolicy, logger: SupersetLogger ) -> set: - """[HELPER] Применение политик хранения файлов - @pre: - - files_with_dates должен содержать валидные даты - @post: - - Возвращает set файлов для сохранения - - Соответствует указанным retention-правилам - """ - # [GROUPING] Группировка файлов - daily_groups = defaultdict(list) - weekly_groups = defaultdict(list) - monthly_groups = defaultdict(list) + """(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.debug(f"[RETENTION_DEBUG] Processing {len(files_with_dates)} files for retention policy") - - for file, file_date in files_with_dates: - daily_groups[file_date].append(file) - weekly_groups[(file_date.isocalendar().year, file_date.isocalendar().week)].append(file) - monthly_groups[(file_date.year, file_date.month)].append(file) - - logger.debug(f"[RETENTION_DEBUG] Grouped into {len(daily_groups)} daily groups, {len(weekly_groups)} weekly groups, {len(monthly_groups)} monthly groups") + logger.info(f"[INFO][RETENTION_POLICY] Файлов для сохранения после применения политики: {len(files_to_keep)}") - # [SELECTION] Выбор файлов для сохранения - keep_files = set() - - # Daily - последние N дней - sorted_daily = sorted(daily_groups.keys(), reverse=True)[:daily] - logger.debug(f"[RETENTION_DEBUG] Daily groups to keep: {sorted_daily}") - for day in sorted_daily: - keep_files.update(daily_groups[day]) + return files_to_keep +# END_FUNCTION_apply_retention_policy - # Weekly - последние N недель - sorted_weekly = sorted(weekly_groups.keys(), reverse=True)[:weekly] - logger.debug(f"[RETENTION_DEBUG] Weekly groups to keep: {sorted_weekly}") - for week in sorted_weekly: - keep_files.update(weekly_groups[week]) - - # Monthly - последние N месяцев - sorted_monthly = sorted(monthly_groups.keys(), reverse=True)[:monthly] - logger.debug(f"[RETENTION_DEBUG] Monthly groups to keep: {sorted_monthly}") - for month in sorted_monthly: - keep_files.update(monthly_groups[month]) - - logger.debug(f"[RETENTION] Сохранено файлов: {len(keep_files)}") - logger.debug(f"[RETENTION_DEBUG] Files to keep: {[f.name for f in keep_files]}") - return keep_files - -# [CONTRACT] Сохранение и распаковка дашборда +# 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], @@ -376,30 +351,23 @@ def save_and_unpack_dashboard( original_filename: Optional[str] = None, logger: Optional[SupersetLogger] = None ) -> Tuple[Path, Optional[Path]]: - """[OPERATION] Обработка ZIP-архива дашборда - @pre: - - zip_content должен быть валидным ZIP - - output_dir должен существовать или быть возможным для создания - @post: - - Возвращает (путь_к_архиву, путь_распаковки) или (путь_к_архиву, None) - - Сохраняет оригинальную структуру файлов - """ + """Сохраняет и опционально распаковывает ZIP-архив дашборда.""" logger = logger or SupersetLogger(name="fileio", console=False) - logger.info(f"Старт обработки дашборда. Распаковка: {unpack}") + logger.info(f"[STATE] Старт обработки дашборда. Распаковка: {unpack}") try: output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) - logger.debug(f"Директория {output_path} создана/проверена") + 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"Сгенерировано имя файла: {zip_name}") + logger.debug(f"[DEBUG] Сгенерировано имя файла: {zip_name}") zip_path = output_path / zip_name - logger.info(f"Сохранение дашборда в: {zip_path}") + logger.info(f"[STATE] Сохранение дашборда в: {zip_path}") with open(zip_path, "wb") as f: f.write(zip_content) @@ -407,650 +375,304 @@ def save_and_unpack_dashboard( if unpack: with zipfile.ZipFile(zip_path, 'r') as zip_ref: zip_ref.extractall(output_path) - logger.info(f"Дашборд распакован в: {output_path}") + logger.info(f"[STATE] Дашборд распакован в: {output_path}") return zip_path, output_path return zip_path, None except zipfile.BadZipFile as e: - logger.error(f"[ZIP_ERROR] Невалидный ZIP-архив: {str(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"[UNPACK_ERROR] Ошибка обработки: {str(e)}", exc_info=True) + logger.error(f"[STATE][UNPACK_ERROR] Ошибка обработки: {str(e)}", exc_info=True) raise +# END_FUNCTION_save_and_unpack_dashboard -def print_directory( - root_dir: str, - logger: Optional[SupersetLogger] = None +# 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 + +# 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: - """[CONTRACT] Визуализация структуры директории в древовидном формате - @pre: - - root_dir должен быть валидным путем к директории - @post: - - Выводит в консоль и логи структуру директории - - Не модифицирует файловую систему - @errors: - - ValueError если путь не существует или не является директорией - """ - logger = logger or SupersetLogger(name="fileio", console=False) - logger.debug(f"[DIR_TREE] Начало построения дерева для {root_dir}") - try: - root_path = Path(root_dir) - - # [VALIDATION] Проверка существования и типа - if not root_path.exists(): - raise ValueError(f"Путь не существует: {root_dir}") - if not root_path.is_dir(): - raise ValueError(f"Указан файл вместо директории: {root_dir}") + with open(file_path, 'r', encoding='utf-8') as f: + data = yaml.safe_load(f) - # [OUTPUT] Форматированный вывод - print(f"\n{root_dir}/") - with os.scandir(root_dir) as entries: - entries = sorted(entries, key=lambda e: e.name) - for idx, entry in enumerate(entries): - is_last = idx == len(entries) - 1 - prefix = " └── " if is_last else " ├── " - suffix = "/" if entry.is_dir() else "" - print(f"{prefix}{entry.name}{suffix}") + updates = {} - logger.info(f"[DIR_TREE] Успешно построено дерево для {root_dir}") + 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'") - except Exception as e: - error_msg = f"[DIR_TREE_ERROR] Ошибка визуализации: {str(e)}" - logger.error(error_msg, exc_info=True) - raise ValueError(error_msg) from e + 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}) не совпадает" + ) -def validate_directory_structure( - root_dir: str, - logger: Optional[SupersetLogger] = None -) -> bool: - """[CONTRACT] Валидация структуры директории экспорта Superset - @pre: - - root_dir должен быть валидным путем - @post: - - Возвращает True если структура соответствует требованиям: - 1. Ровно один подкаталог верхнего уровня - 2. Наличие metadata.yaml - 3. Допустимые имена поддиректорий (databases/datasets/charts/dashboards) - @errors: - - ValueError при некорректном пути - """ - logger = logger or SupersetLogger(name="fileio", console=False) - logger.info(f"[DIR_VALIDATION] Валидация структуры в {root_dir}") + 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 - try: - root_path = Path(root_dir) - - # [BASE VALIDATION] - if not root_path.exists(): - raise ValueError(f"Директория не существует: {root_dir}") - if not root_path.is_dir(): - raise ValueError(f"Требуется директория, получен файл: {root_dir}") + 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] - root_items = os.listdir(root_dir) - - # [CHECK 1] Ровно один подкаталог верхнего уровня - if len(root_items) != 1: - logger.warning(f"[VALIDATION_FAIL] Ожидается 1 подкаталог, найдено {len(root_items)}") - return False + if updates: + logger.info(f"[STATE] Обновление {file_path}: {updates}") + data.update(updates) - subdir_path = root_path / root_items[0] - - # [CHECK 2] Должен быть подкаталог - if not subdir_path.is_dir(): - logger.warning(f"[VALIDATION_FAIL] {root_items[0]} не является директорией") - return False + with open(file_path, 'w', encoding='utf-8') as file: + yaml.dump( + data, + file, + default_flow_style=False, + sort_keys=False + ) - # [CHECK 3] Проверка metadata.yaml - if "metadata.yaml" not in os.listdir(subdir_path): - logger.warning("[VALIDATION_FAIL] Отсутствует metadata.yaml") - return False + except yaml.YAMLError as e: + logger.error(f"[STATE][YAML_ERROR] Ошибка парсинга {file_path}: {str(e)}") +# END_FUNCTION__update_yaml_file - # [CHECK 4] Валидация поддиректорий - found_folders = set() - for item in os.listdir(subdir_path): - if item == "metadata.yaml": - continue - - item_path = subdir_path / item - if not item_path.is_dir(): - logger.warning(f"[VALIDATION_FAIL] {item} не является директорией") - return False - - if item not in ALLOWED_FOLDERS: - logger.warning(f"[VALIDATION_FAIL] Недопустимая директория: {item}") - return False - - if item in found_folders: - logger.warning(f"[VALIDATION_FAIL] Дубликат директории: {item}") - return False - - found_folders.add(item) - - # [FINAL CHECK] - valid_structure = ( - 1 <= len(found_folders) <= 4 and - all(folder in ALLOWED_FOLDERS for folder in found_folders) - ) - - if not valid_structure: - logger.warning( - f"[VALIDATION_FAIL] Некорректный набор директорий: {found_folders}" - ) - - return valid_structure - - except Exception as e: - error_msg = f"[DIR_VALIDATION_ERROR] Критическая ошибка: {str(e)}" - logger.error(error_msg, exc_info=True) - raise ValueError(error_msg) from e - -# [CONTRACT] Создание ZIP-архива -def create_dashboard_export( - zip_path: Union[str, Path], - source_paths: List[Union[str, Path]], - exclude_extensions: Optional[List[str]] = None, - validate_source: bool = False, - logger: Optional[SupersetLogger] = None -) -> bool: - """[OPERATION] Упаковка дашборда в архив - @pre: - - source_paths должны существовать - - Должны быть права на запись в zip_path - @post: - - Возвращает True если создание успешно - - Сохраняет оригинальную структуру папок - """ - logger = logger or SupersetLogger(name="fileio", console=False) - logger.info(f"Упаковка дашбордов: {source_paths} -> {zip_path}") - - try: - exclude_ext = [ext.lower() for ext in exclude_extensions] if exclude_extensions else [] - - 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('*'): - if item.is_file() and item.suffix.lower() not in exclude_ext: - arcname = item.relative_to(path.parent) - zipf.write(item, arcname) - logger.debug(f"Добавлен в архив: {arcname}") - - logger.info(f"Архив создан: {zip_path}") - return True - - except Exception as e: - logger.error(f"[ZIP_CREATION_ERROR] Ошибка: {str(e)}", exc_info=True) - return False - - -# [UTILITY] Валидация имен файлов -def sanitize_filename(filename: str) -> str: - """[UTILITY] Очистка имени файла от опасных символов - @post: - - Возвращает безопасное имя файла без спецсимволов - """ - return re.sub(r'[\\/*?:"<>|]', "_", filename).strip() - - -def get_filename_from_headers(headers: dict) -> Optional[str]: - """Извлекает имя файла из заголовков HTTP-ответа""" - content_disposition = headers.get("Content-Disposition", "") - - # Пытаемся найти имя файла в кавычках - filename_match = re.findall(r'filename="(.+?)"', content_disposition) - if not filename_match: - # Пробуем без кавычек - filename_match = re.findall(r'filename=([^;]+)', content_disposition) - - if filename_match: - return filename_match[0].strip('"') - return None - -def determine_and_load_yaml_type(file_path): - with open(file_path, 'r') as f: - data = yaml.safe_load(f) - - if 'dashboard_title' in data and 'position' in data: - return data, 'dashboard' - elif 'sqlalchemy_uri' in data and 'database_name' in data: - return data, 'database' - elif 'table_name' in data and ('sql' in data or 'columns' in data): - return data, 'dataset' - elif 'slice_name' in data and 'viz_type' in data: - return data, 'chart' - else: - return data, 'unknown' - -# [CONTRACT] Управление конфигурациями YAML +# [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, + db_configs: Optional[List[Dict]] = None, path: str = "dashboards", regexp_pattern: Optional[LiteralString] = None, replace_string: Optional[LiteralString] = None, logger: Optional[SupersetLogger] = None ) -> None: - """ - [OPERATION] Обновление YAML-конфигов - @pre: - - path должен содержать валидные YAML-файлы - - db_configs должен содержать old/new состояния - @post: - - Все YAML-файлы обновлены согласно конфигурациям - - Сохраняется оригинальная структура файлов - - Обновляет конфигурации в YAML-файлах баз данных, заменяя старые значения на новые. - Поддерживает два типа замен: - 1. Точечную замену значений по ключам из db_config - 2. Регулярные выражения для замены текста во всех строковых полях - - Параметры: - :db_configs: Список словарей или словарь с параметрами для замены в формате: - { - "old": {старые_ключи: значения_для_поиска}, - "new": {новые_ключи: значения_для_замены} - } - Если не указан - используется только замена по регулярным выражениям - :path: Путь к папке с YAML-файлами (по умолчанию "dashboards") - :regexp_pattern: Регулярное выражение для поиска текста (опционально) - :replace_string: Строка для замены найденного текста (используется с regexp_pattern) - :logger: Логгер для записи событий (по умолчанию создается новый) - - Логирует: - - Информационные сообщения о начале процесса и успешных обновлениях - - Ошибки обработки отдельных файлов - - Критические ошибки, прерывающие выполнение - - Пример использования: - update_yamls( - db_config={ - "old": {"host": "old.db.example.com"}, - "new": {"host": "new.db.example.com"} - }, - regexp_pattern="old\.", - replace_string="new." - ) - """ - logger = logger or SupersetLogger(name="fileio", console=False) - logger.info("[YAML_UPDATE] Старт обновления конфигураций") + 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) - if not dir.exists() or not dir.is_dir(): + try: + dir_path = Path(path) + + if not dir_path.exists() or not dir_path.is_dir(): raise FileNotFoundError(f"Путь {path} не существует или не является директорией") - - yaml_files = dir.rglob("*.yaml") + + yaml_files = dir_path.rglob("*.yaml") for file_path in yaml_files: - try: - result = determine_and_load_yaml_type(file_path) - - data, yaml_type = result if result else ({}, None) - logger.debug(f"Тип {file_path} - {yaml_type}") + _update_yaml_file(file_path, db_configs, regexp_pattern, replace_string, logger) - updates = {} - - # 1. Обработка замен по ключам из db_config (если нужно использовать только новые значения) - 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 # Заменяем без проверки старого значения - - # 2. Регулярная замена (с исправленной функцией process_value) - if regexp_pattern: - def process_value(value: Any) -> 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 - elif isinstance(value, dict): - new_dict = {} - for k, v in value.items(): - sub_matched, sub_val = process_value(v) - new_dict[k] = sub_val - if sub_matched: - matched = True - return matched, new_dict - elif isinstance(value, list): - new_list = [] - for item in value: - sub_matched, sub_val = process_value(item) - new_list.append(sub_val) - if sub_matched: - matched = True - return matched, new_list - return False, value # Нет замены для других типов - - # Применяем обработку ко всем данным - _, processed_data = process_value(data) - # Собираем обновления только для изменившихся полей - for key in processed_data: - if processed_data[key] != data.get(key): - updates[key] = processed_data[key] - - if updates: - logger.info(f"Обновление {file_path}: {updates}") - data.update(updates) - - with open(file_path, 'w') as file: - yaml.dump( - data, - file, - default_flow_style=False, - sort_keys=False - ) - - except yaml.YAMLError as e: - logger.error(f"[YAML_ERROR] Ошибка парсинга {file_path}: {str(e)}") - - except Exception as e: - logger.error(f"[YAML_UPDATE_ERROR] Критическая ошибка: {str(e)}", exc_info=True) + 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 [] + + 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('*'): + if item.is_file() and item.suffix.lower() not in exclude_ext: + arcname = item.relative_to(path.parent) + zipf.write(item, arcname) + logger.debug(f"[DEBUG] Добавлен в архив: {arcname}") + + logger.info(f"[STATE]архив создан: {zip_path}") + return True + + except (IOError, zipfile.BadZipFile) as e: + logger.error(f"[STATE][ZIP_CREATION_ERROR] Ошибка: {str(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 +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] +def get_filename_from_headers(headers: dict) -> Optional[str]: + content_disposition = headers.get("Content-Disposition", "") + filename_match = re.findall(r'filename="(.+?)"', content_disposition) + if not filename_match: + filename_match = re.findall(r'filename=([^;]+)', content_disposition) + if filename_match: + return filename_match[0].strip('"') + return None +# END_FUNCTION_get_filename_from_headers + +# [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 def consolidate_archive_folders(root_directory: Path, logger: Optional[SupersetLogger] = None) -> None: - """ - Consolidates dashboard folders under a root directory based on the slug (pattern MM-0080 - two latin letters - hyphen - 4 digits) - and moves the contents to the folder with the latest modification date. - - Args: - root_directory (Path): The root directory containing the dashboard folders. - - Raises: - TypeError: If root_directory is not a Path object. - ValueError: If root_directory is empty. - - [CONTRACT] - @pre: root_directory must be a valid Path object representing an existing directory. - @post: The contents of all folders matching the slug pattern are moved to the folder with the latest modification date for each slug. - @invariant: The slug pattern remains consistent throughout the execution. - @raise: TypeError if root_directory is not a Path, ValueError if root_directory is empty. - """ - - # [CONTRACT] Ensure valid input + 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: - raise ValueError("root_directory cannot be empty.") - - logger.debug(f"[DEBUG] Checking root_folder: {root_directory}") + if not root_directory.is_dir(): + raise ValueError("root_directory must be an existing directory.") - # [SECTION] Define the slug pattern - slug_pattern = re.compile(r"([A-Z]{2}-\d{4})") # Capture the first occurrence of the pattern + logger.debug("[DEBUG] Checking root_folder: {root_directory}") + + slug_pattern = re.compile(r"([A-Z]{2}-\d{4})") - # [SECTION] Group folders by slug 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}") # Debug log: show the folder name being checked + logger.debug(f"[DEBUG] Checking folder: {folder_name}") match = slug_pattern.search(folder_name) if match: - slug = match.group(1) # Extract the captured group (the slug) - logger.info(f"[INFO] Found slug: {slug} in folder: {folder_name}") #Log when slug is matched - logger.debug(f"[DEBUG] Regex match object: {match}") # Log the complete match object + 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}") # Debug log: show when slug is not matched + logger.debug(f"[DEBUG] No slug found in folder: {folder_name}") else: - logger.debug(f"[DEBUG] Not a directory: {folder_name}") #debug log for when its not a directory + logger.debug(f"[DEBUG] Not a directory: {folder_name}") - # [SECTION] Check if any slugs were found if not dashboards_by_slug: - logger.warning("[WARN] No folders found matching the slug pattern.") + logger.warning("[STATE] No folders found matching the slug pattern.") return - # [SECTION] Iterate through each slug group for slug, folder_list in dashboards_by_slug.items(): - # [ACTION] Find the folder with the latest modification date latest_folder = max(folder_list, key=os.path.getmtime) - logger.info(f"[INFO] Latest folder for slug {slug}: {latest_folder}") + logger.info(f"[STATE] Latest folder for slug {slug}: {latest_folder}") - # [SECTION] Move contents of other folders to the latest folder for folder in folder_list: if folder != latest_folder: - # [ACTION] Move contents try: for item in os.listdir(folder): s = os.path.join(folder, item) d = os.path.join(latest_folder, item) - if os.path.isdir(s): - shutil.move(s, d) - else: - shutil.move(s, d) + 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(f"[INFO] Moved contents of {folder} to {latest_folder}") - except Exception as e: - logger.error(f"[ERROR] Failed to move contents of {folder} to {latest_folder}: {e}", exc_info=True) - - logger.info("[INFO] Dashboard consolidation completed.") - # [COHERENCE_CHECK_PASSED] Function executed successfully and all contracts were met. - -def sync_for_git( - source_path: str, - destination_path: str, - dry_run: bool = False, - logger: Optional[SupersetLogger] = None -) -> None: - """[CONTRACT] Синхронизация контента между директориями с учетом Git - @pre: - - source_path должен существовать и быть директорией - - destination_path должен быть допустимым путем - - Исходная директория должна содержать валидную структуру Superset - @post: - - Полностью заменяет содержимое destination_path (кроме .git) - - Сохраняет оригинальные разрешения файлов - - Логирует все изменения при dry_run=True - @errors: - - ValueError при несоответствии структуры source_path - - RuntimeError при ошибках файловых операций - """ - logger = logger or SupersetLogger(name="fileio", console=False) - logger.info( - "[SYNC_START] Запуск синхронизации", - extra={ - "source": source_path, - "destination": destination_path, - "dry_run": dry_run - } - ) - - try: - # [VALIDATION] Проверка исходной директории - if not validate_directory_structure(source_path, logger): - raise ValueError(f"Invalid source structure: {source_path}") - - src_path = Path(source_path) - dst_path = Path(destination_path) - - # [PREPARATION] Сбор информации о файлах - source_files = get_file_mapping(src_path) - destination_files = get_file_mapping(dst_path) - - # [SYNC OPERATIONS] - operations = { - 'copied': 0, - 'removed': 0, - 'skipped': 0 - } - - # Копирование/обновление файлов - operations.update(process_copy_operations( - src_path, - dst_path, - source_files, - destination_files, - dry_run, - logger - )) - - # Удаление устаревших файлов - operations.update(process_cleanup_operations( - dst_path, - source_files, - destination_files, - dry_run, - logger - )) - - # [RESULT] - logger.info( - "[SYNC_RESULT] Итоги синхронизации", - extra=operations - ) - - except Exception as e: - error_msg = f"[SYNC_FAILED] Ошибка синхронизации: {str(e)}" - logger.error(error_msg, exc_info=True) - raise RuntimeError(error_msg) from e - - -# [HELPER] Получение карты файлов -def get_file_mapping(root_path: Path) -> Dict[str, Path]: - """[UTILITY] Генерация словаря файлов относительно корня - @post: - - Возвращает Dict[relative_path: Path] - - Игнорирует .git директории - """ - file_map = {} - for item in root_path.rglob("*"): - if ".git" in item.parts: - continue - rel_path = item.relative_to(root_path) - file_map[str(rel_path)] = item - return file_map - - -# [HELPER] Обработка копирования -def process_copy_operations( - src_path: Path, - dst_path: Path, - source_files: Dict[str, Path], - destination_files: Dict[str, Path], - dry_run: bool, - logger: SupersetLogger -) -> Dict[str, int]: - """[OPERATION] Выполнение операций копирования - @post: - - Возвращает счетчики операций {'copied': X, 'skipped': Y} - - Создает все необходимые поддиректории - """ - counters = {'copied': 0, 'skipped': 0} - - for rel_path, src_file in source_files.items(): - dst_file = dst_path / rel_path - - # Проверка необходимости обновления - if rel_path in destination_files: - if filecmp.cmp(src_file, dst_file, shallow=False): - counters['skipped'] += 1 - continue - - # Dry-run логирование - if dry_run: - logger.debug( - f"[DRY_RUN] Будет скопирован: {rel_path}", - extra={'operation': 'copy'} - ) - continue - - # Реальное копирование - try: - dst_file.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(src_file, dst_file) - counters['copied'] += 1 - logger.debug(f"Скопирован: {rel_path}") - except Exception as copy_error: - logger.error( - f"[COPY_ERROR] Ошибка копирования {rel_path}: {str(copy_error)}", - exc_info=True - ) - raise - - return counters - - -# [HELPER] Обработка удаления -def process_cleanup_operations( - dst_path: Path, - source_files: Dict[str, Path], - destination_files: Dict[str, Path], - dry_run: bool, - logger: SupersetLogger -) -> Dict[str, int]: - """[OPERATION] Удаление устаревших файлов - @post: - - Возвращает счетчики {'removed': X} - - Гарантированно не удаляет .git - """ - counters = {'removed': 0} - files_to_delete = set(destination_files.keys()) - set(source_files.keys()) - git_dir = dst_path / ".git" - - for rel_path in files_to_delete: - target = dst_path / rel_path - - # Защита .git - try: - if git_dir in target.parents or target == git_dir: - logger.debug(f"Сохранен .git: {target}") - continue - except ValueError: # Для случаев некорректных путей - continue - - # Dry-run логирование - if dry_run: - logger.debug( - f"[DRY_RUN] Будет удален: {rel_path}", - extra={'operation': 'delete'} - ) - continue - - # Реальное удаление - try: - if target.is_file(): - target.unlink() - elif target.is_dir(): - shutil.rmtree(target) - counters['removed'] += 1 - logger.debug(f"Удален: {rel_path}") - except Exception as remove_error: - logger.error( - f"[REMOVE_ERROR] Ошибка удаления {target}: {str(remove_error)}", - exc_info=True - ) - raise - - return counters + logger.info("[STATE] Dashboard consolidation completed.") +# END_FUNCTION_consolidate_archive_folders +# END_MODULE_fileio \ No newline at end of file diff --git a/superset_tool/utils/init_clients.py b/superset_tool/utils/init_clients.py index 8d86316..5fe51a4 100644 --- a/superset_tool/utils/init_clients.py +++ b/superset_tool/utils/init_clients.py @@ -1,100 +1,71 @@ -# [MODULE] Superset Init clients -# @contract: Автоматизирует процесс инициализации клиентов для использования скриптами. -# @semantic_layers: -# 1. Инициализация логгера и клиентов Superset. -# @coherence: -# - Использует `SupersetClient` для взаимодействия с API Superset. -# - Использует `SupersetLogger` для централизованного логирования. -# - Интегрируется с `keyring` для безопасного хранения паролей. - -# [IMPORTS] Стандартная библиотека -import logging -from datetime import datetime -from pathlib import Path +# [MODULE] Superset Clients Initializer +# PURPOSE: Централизованно инициализирует клиенты Superset для различных окружений (DEV, PROD, SBX, PREPROD). +# COHERENCE: +# - Использует `SupersetClient` для создания экземпляров клиентов. +# - Использует `SupersetLogger` для логирования процесса. +# - Интегрируется с `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 - -# [FUNCTION] setup_clients -# @contract: Инициализирует и возвращает SupersetClient для каждого заданного окружения. -# @pre: -# - `keyring` должен содержать необходимые пароли для "dev migrate", "prod migrate", "sandbox migrate". -# - `logger` должен быть инициализирован. -# @post: -# - Возвращает словарь {env_name: SupersetClient_instance}. -# - Логирует успешную инициализацию или ошибку. -# @raise: -# - `Exception`: При любой ошибке в процессе инициализации клиентов (например, отсутствие пароля в keyring, проблемы с сетью при первой аутентификации). -def setup_clients(logger: 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` при любой ошибке (например, отсутствие пароля, ошибка подключения). +def setup_clients(logger: SupersetLogger) -> Dict[str, SupersetClient]: + """Инициализирует и настраивает клиенты для всех окружений Superset.""" # [ANCHOR] CLIENTS_INITIALIZATION + logger.info("[INFO][INIT_CLIENTS_START] Запуск инициализации клиентов Superset.") clients = {} + + environments = { + "dev": "https://devta.bi.dwh.rusal.com/api/v1", + "prod": "https://prodta.bi.dwh.rusal.com/api/v1", + "sbx": "https://sandboxta.bi.dwh.rusal.com/api/v1", + "preprod": "https://preprodta.bi.dwh.rusal.com/api/v1" + } + try: - # [INFO] Инициализация конфигурации для Dev - dev_config = SupersetConfig( - base_url="https://devta.bi.dwh.rusal.com/api/v1", - auth={ - "provider": "db", - "username": "migrate_user", - "password": keyring.get_password("system", "dev migrate"), - "refresh": True - }, - verify_ssl=False - ) - # [DEBUG] Dev config created: {dev_config.base_url} + for env_name, base_url in environments.items(): + logger.debug(f"[DEBUG][CONFIG_CREATE] Создание конфигурации для окружения: {env_name.upper()}") + password = keyring.get_password("system", f"{env_name} migrate") + if not password: + raise ValueError(f"Пароль для '{env_name} migrate' не найден в keyring.") - # [INFO] Инициализация конфигурации для Prod - prod_config = SupersetConfig( - base_url="https://prodta.bi.dwh.rusal.com/api/v1", - auth={ - "provider": "db", - "username": "migrate_user", - "password": keyring.get_password("system", "prod migrate"), - "refresh": True - }, - verify_ssl=False - ) - # [DEBUG] Prod config created: {prod_config.base_url} + config = SupersetConfig( + base_url=base_url, + 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()} успешно создан.") - # [INFO] Инициализация конфигурации для Sandbox - sandbox_config = SupersetConfig( - base_url="https://sandboxta.bi.dwh.rusal.com/api/v1", - auth={ - "provider": "db", - "username": "migrate_user", - "password": keyring.get_password("system", "sandbox migrate"), - "refresh": True - }, - verify_ssl=False - ) - # [DEBUG] Sandbox config created: {sandbox_config.base_url} - - # [INFO] Инициализация конфигурации для Preprod - preprod_config = SupersetConfig( - base_url="https://preprodta.bi.dwh.rusal.com/api/v1", - auth={ - "provider": "db", - "username": "migrate_user", - "password": keyring.get_password("system", "preprod migrate"), - "refresh": True - }, - verify_ssl=False - ) - # [DEBUG] Sandbox config created: {sandbox_config.base_url} - - # [INFO] Создание экземпляров SupersetClient - clients['dev'] = SupersetClient(dev_config, logger) - clients['sbx'] = SupersetClient(sandbox_config,logger) - clients['prod'] = SupersetClient(prod_config,logger) - clients['preprod'] = SupersetClient(preprod_config,logger) - logger.info("[COHERENCE_CHECK_PASSED] Клиенты для окружений успешно инициализированы", extra={"envs": list(clients.keys())}) + logger.info(f"[COHERENCE_CHECK_PASSED][INIT_CLIENTS_SUCCESS] Все клиенты ({', '.join(clients.keys())}) успешно инициализированы.") return clients + except Exception as e: - logger.error(f"[ERROR] Ошибка инициализации клиентов: {str(e)}", exc_info=True) - raise \ No newline at end of file + logger.error(f"[CRITICAL][INIT_CLIENTS_FAILED] Ошибка при инициализации клиентов: {str(e)}", exc_info=True) + raise +# END_FUNCTION_setup_clients +# END_MODULE_init_clients \ No newline at end of file diff --git a/superset_tool/utils/logger.py b/superset_tool/utils/logger.py index 0e1fd6b..59111f0 100644 --- a/superset_tool/utils/logger.py +++ b/superset_tool/utils/logger.py @@ -1,9 +1,6 @@ # [MODULE] Superset Tool Logger Utility -# @contract: Этот модуль предоставляет утилиту для настройки логирования в приложении. -# @semantic_layers: -# - [CONFIG]: Настройка логгера. -# - [UTILITY]: Вспомогательные функции. -# @coherence: Модуль должен быть семантически когерентен со стандартной библиотекой `logging`. +# PURPOSE: Предоставляет стандартизированный класс-обертку `SupersetLogger` для настройки и использования логирования в проекте. +# COHERENCE: Модуль согласован со стандартной библиотекой `logging`, расширяя ее для нужд проекта. import logging import sys @@ -11,8 +8,20 @@ from datetime import datetime from pathlib import Path from typing import Optional -# [CONSTANTS] - +# CONTRACT: +# PURPOSE: Обеспечивает унифицированную настройку логгера с выводом в консоль и/или файл. +# PRECONDITIONS: +# - `name` должен быть строкой. +# - `level` должен быть валидным уровнем логирования (например, `logging.INFO`). +# POSTCONDITIONS: +# - Создает и настраивает логгер с указанным именем и уровнем. +# - Добавляет обработчики для вывода в файл (если указан `log_dir`) и в консоль (если `console=True`). +# - Очищает все предыдущие обработчики для данного логгера, чтобы избежать дублирования. +# PARAMETERS: +# - name: str - Имя логгера. +# - log_dir: Optional[Path] - Директория для сохранения лог-файлов. +# - level: int - Уровень логирования. +# - console: bool - Флаг для включения вывода в консоль. class SupersetLogger: def __init__( self, @@ -23,34 +32,40 @@ class SupersetLogger: ): self.logger = logging.getLogger(name) self.logger.setLevel(level) - + formatter = logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s' ) - # Очищаем существующие обработчики - if self.logger.handlers: - for handler in self.logger.handlers[:]: - self.logger.removeHandler(handler) + # [ANCHOR] HANDLER_RESET + # Очищаем существующие обработчики, чтобы избежать дублирования вывода при повторной инициализации. + if self.logger.hasHandlers(): + self.logger.handlers.clear() - # Файловый обработчик + # [ANCHOR] FILE_HANDLER 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}_{self._get_timestamp()}.log" + log_dir / f"{name}_{timestamp}.log", encoding='utf-8' ) file_handler.setFormatter(formatter) self.logger.addHandler(file_handler) - # Консольный обработчик + # [ANCHOR] CONSOLE_HANDLER if console: - console_handler = logging.StreamHandler() + console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(formatter) self.logger.addHandler(console_handler) - + + # CONTRACT: + # PURPOSE: (HELPER) Генерирует строку с текущей датой для имени лог-файла. + # RETURN: str - Отформатированная дата (YYYYMMDD). def _get_timestamp(self) -> str: return datetime.now().strftime("%Y%m%d") + # END_FUNCTION__get_timestamp + # [INTERFACE] Методы логирования def info(self, message: str, extra: Optional[dict] = None, exc_info: bool = False): self.logger.info(message, extra=extra, exc_info=exc_info) @@ -59,47 +74,15 @@ class SupersetLogger: def warning(self, message: str, extra: Optional[dict] = None, exc_info: bool = False): self.logger.warning(message, extra=extra, exc_info=exc_info) - + def critical(self, message: str, extra: Optional[dict] = None, exc_info: bool = False): self.logger.critical(message, extra=extra, exc_info=exc_info) def debug(self, message: str, extra: Optional[dict] = None, exc_info: bool = False): self.logger.debug(message, extra=extra, exc_info=exc_info) - def exception(self, message: str): - self.logger.exception(message) + def exception(self, message: str, *args, **kwargs): + self.logger.exception(message, *args, **kwargs) +# END_CLASS_SupersetLogger -def setup_logger(name: str, level: int = logging.INFO) -> logging.Logger: - # [FUNCTION] setup_logger - # [CONTRACT] - """ - Настраивает и возвращает логгер с заданным именем и уровнем. - - @pre: - - `name` является непустой строкой. - - `level` является допустимым уровнем логирования из модуля `logging`. - @post: - - Возвращает настроенный экземпляр `logging.Logger`. - - Логгер имеет StreamHandler, выводящий в sys.stdout. - - Форматтер логгера включает время, уровень, имя и сообщение. - @side_effects: - - Создает и добавляет StreamHandler к логгеру. - @invariant: - - Логгер с тем же именем всегда возвращает один и тот же экземпляр. - """ - # [CONFIG] Настройка логгера - # [COHERENCE_CHECK_PASSED] Логика настройки соответствует описанию. - logger = logging.getLogger(name) - logger.setLevel(level) - - # Создание форматтера - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s') - - # Проверка наличия существующих обработчиков - if not logger.handlers: - # Создание StreamHandler для вывода в sys.stdout - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(formatter) - logger.addHandler(handler) - - return logger +# END_MODULE_logger diff --git a/superset_tool/utils/network.py b/superset_tool/utils/network.py index da26385..062e5fd 100644 --- a/superset_tool/utils/network.py +++ b/superset_tool/utils/network.py @@ -1,15 +1,11 @@ -# [MODULE] Сетевой клиент для API -# @contract: Инкапсулирует низкоуровневую HTTP-логику, аутентификацию, повторные попытки и обработку сетевых ошибок. -# @semantic_layers: -# 1. Инициализация сессии `requests` с настройками SSL и таймаутов. -# 2. Управление аутентификацией (получение и обновление access/CSRF токенов). -# 3. Выполнение HTTP-запросов (GET, POST и т.д.) с автоматическими заголовками. -# 4. Обработка пагинации для API-ответов. -# 5. Обработка загрузки файлов. -# @coherence: -# - Полностью независим от `SupersetClient`, предоставляя ему чистый API для сетевых операций. -# - Использует `SupersetLogger` для внутреннего логирования. -# - Всегда выбрасывает типизированные исключения из `superset_tool.exceptions`. +# -*- coding: utf-8 -*- +# pylint: disable=too-many-arguments,too-many-locals,too-many-statements,too-many-branches,unused-argument +""" +[MODULE] Сетевой клиент для API + +[DESCRIPTION] +Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API. +""" # [IMPORTS] Стандартная библиотека from typing import Optional, Dict, Any, BinaryIO, List, Union @@ -19,173 +15,106 @@ from pathlib import Path # [IMPORTS] Сторонние библиотеки import requests -import urllib3 # Для отключения SSL-предупреждений +import urllib3 # Для отключения SSL-предупреждений # [IMPORTS] Локальные модули -from ..exceptions import AuthenticationError, NetworkError, DashboardNotFoundError, SupersetAPIError, PermissionDeniedError -from .logger import SupersetLogger # Импорт логгера +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 class APIClient: - """[NETWORK-CORE] Инкапсулирует HTTP-логику для работы с API. - @contract: - - Гарантирует retry-механизмы для запросов. - - Выполняет SSL-валидацию или отключает ее по конфигурации. - - Автоматически управляет access и CSRF токенами. - - Преобразует HTTP-ошибки в типизированные исключения `superset_tool.exceptions`. - @pre: - - `base_url` должен быть валидным URL. - - `auth` должен содержать необходимые данные для аутентификации. - - `logger` должен быть инициализирован. - @post: - - Аутентификация выполняется при первом запросе или явно через `authenticate()`. - - `self._tokens` всегда содержит актуальные access/CSRF токены после успешной аутентификации. - @invariant: - - Сессия `requests` активна и настроена. - - Все запросы используют актуальные токены. - """ + """[NETWORK-CORE] Инкапсулирует HTTP-логику для работы с API.""" + def __init__( self, - base_url: str, - auth: Dict[str, Any], + config: Dict[str, Any], verify_ssl: bool = True, - timeout: int = 30, + timeout: int = DEFAULT_TIMEOUT, logger: Optional[SupersetLogger] = None ): - # [INIT] Основные параметры - self.base_url = base_url - self.auth = auth - self.verify_ssl = verify_ssl - self.timeout = timeout - self.logger = logger or SupersetLogger(name="APIClient") # [COHERENCE_CHECK_PASSED] Инициализация логгера - - # [INIT] Сессия Requests + self.logger = logger or SupersetLogger(name="APIClient") + self.logger.info("[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.session = self._init_session() - self._tokens: Dict[str, str] = {} # [STATE] Хранилище токенов - self._authenticated = False # [STATE] Флаг аутентификации - - self.logger.debug( - "[INIT] APIClient инициализирован.", - extra={"base_url": self.base_url, "verify_ssl": self.verify_ssl} - ) + self._tokens: Dict[str, str] = {} + self._authenticated = False + self.logger.info("[INFO][APIClient.__init__][SUCCESS] APIClient initialized.") def _init_session(self) -> requests.Session: - """[HELPER] Настройка сессии `requests` с адаптерами и SSL-опциями. - @semantic: Создает и конфигурирует объект `requests.Session`. - """ + self.logger.debug("[DEBUG][APIClient._init_session][ENTER] Initializing session.") session = requests.Session() - # [CONTRACT] Настройка повторных попыток 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"} ) - session.mount('http://', requests.adapters.HTTPAdapter(max_retries=retries)) - session.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries)) - - session.verify = self.verify_ssl - if not self.verify_ssl: + 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: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - self.logger.warning("[SECURITY] Отключена проверка SSL-сертификатов. Не использовать в продакшене без явной необходимости.") + self.logger.warning("[WARNING][APIClient._init_session][STATE_CHANGE] SSL verification disabled.") + self.logger.debug("[DEBUG][APIClient._init_session][SUCCESS] Session initialized.") return session def authenticate(self) -> Dict[str, str]: - """[AUTH-FLOW] Получение access и CSRF токенов. - @pre: - - `self.auth` содержит валидные учетные данные. - @post: - - `self._tokens` обновлен актуальными токенами. - - Возвращает обновленные токены. - - `self._authenticated` устанавливается в `True`. - @raise: - - `AuthenticationError`: При ошибках аутентификации (неверные credentials, проблемы с API security). - - `NetworkError`: При проблемах с сетью. - """ - self.logger.info(f"[AUTH] Попытка аутентификации для {self.base_url}") + self.logger.info(f"[INFO][APIClient.authenticate][ENTER] Authenticating to {self.base_url}") try: - # Шаг 1: Получение access_token login_url = f"{self.base_url}/security/login" response = self.session.post( login_url, - json=self.auth, # Используем self.auth, который уже имеет "provider": "db", "refresh": True - timeout=self.timeout + json=self.auth, + timeout=self.request_settings.get("timeout", DEFAULT_TIMEOUT) ) - response.raise_for_status() # Выбросит HTTPError для 4xx/5xx ответов + response.raise_for_status() access_token = response.json()["access_token"] - self.logger.debug("[AUTH] Access token успешно получен.") - - # Шаг 2: Получение CSRF токена csrf_url = f"{self.base_url}/security/csrf_token/" csrf_response = self.session.get( csrf_url, headers={"Authorization": f"Bearer {access_token}"}, - timeout=self.timeout + timeout=self.request_settings.get("timeout", DEFAULT_TIMEOUT) ) csrf_response.raise_for_status() csrf_token = csrf_response.json()["result"] - self.logger.debug("[AUTH] CSRF token успешно получен.") - - # [STATE] Сохранение токенов и обновление флага self._tokens = { "access_token": access_token, "csrf_token": csrf_token } self._authenticated = True - self.logger.info("[COHERENCE_CHECK_PASSED] Аутентификация успешно завершена.") + self.logger.info("[INFO][APIClient.authenticate][SUCCESS] Authenticated successfully.") return self._tokens - except requests.exceptions.HTTPError as e: - error_msg = f"HTTP Error during authentication: {e.response.status_code} - {e.response.text}" - self.logger.error(f"[AUTH_FAILED] {error_msg}", exc_info=True) - if e.response.status_code == 401: # Unauthorized - raise AuthenticationError( - f"Неверные учетные данные или истекший токен.", - url=login_url, username=self.auth.get("username"), - status_code=e.response.status_code, response_text=e.response.text - ) from e - elif e.response.status_code == 403: # Forbidden - raise PermissionDeniedError( - "Недостаточно прав для аутентификации.", - url=login_url, username=self.auth.get("username"), - status_code=e.response.status_code, response_text=e.response.text - ) from e - else: - raise SupersetAPIError( - f"API ошибка при аутентификации: {error_msg}", - url=login_url, status_code=e.response.status_code, response_text=e.response.text - ) from e - except requests.exceptions.RequestException as e: - self.logger.error(f"[NETWORK_ERROR] Сетевая ошибка при аутентификации: {str(e)}", exc_info=True) - raise NetworkError(f"Ошибка сети при аутентификации: {str(e)}", url=login_url) from e - except KeyError as e: - self.logger.error(f"[AUTH_FAILED] Некорректный формат ответа при аутентификации: {str(e)}", exc_info=True) - raise AuthenticationError(f"Некорректный формат ответа API при аутентификации: {str(e)}") from e - except Exception as e: - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка аутентификации: {str(e)}", exc_info=True) - raise AuthenticationError(f"Непредвиденная ошибка аутентификации: {str(e)}") from 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]: - """[INTERFACE] Возвращает стандартные заголовки с текущими токенами. - @semantic: Если токены не получены, пытается выполнить аутентификацию. - @post: Всегда возвращает словарь с 'Authorization' и 'X-CSRFToken'. - @raise: `AuthenticationError` если аутентификация невозможна. - """ if not self._authenticated: - self.authenticate() # Попытка аутентификации при первом запросе заголовков - - # [CONTRACT] Проверка наличия токенов - if not self._tokens or "access_token" not in self._tokens or "csrf_token" not in self._tokens: - self.logger.error("[CONTRACT_VIOLATION] Токены отсутствуют после попытки аутентификации.", extra={"tokens": self._tokens}) - raise AuthenticationError("Не удалось получить токены для заголовков.") - + self.authenticate() return { "Authorization": f"Bearer {self._tokens['access_token']}", - "X-CSRFToken": self._tokens["csrf_token"], + "X-CSRFToken": self._tokens.get("csrf_token", ""), "Referer": self.base_url, "Content-Type": "application/json" } @@ -198,180 +127,95 @@ class APIClient: raw_response: bool = False, **kwargs ) -> Union[requests.Response, Dict[str, Any]]: - """[NETWORK-CORE] Обертка для всех HTTP-запросов к Superset API. - @semantic: - - Выполняет запрос с заданными параметрами. - - Автоматически добавляет базовые заголовки (токены, CSRF). - - Обрабатывает HTTP-ошибки и преобразует их в типизированные исключения. - - В случае 401/403, пытается обновить токен и повторить запрос один раз. - @pre: - - `method` - валидный HTTP-метод ('GET', 'POST', 'PUT', 'DELETE'). - - `endpoint` - валидный путь API. - @post: - - Возвращает объект `requests.Response` (если `raw_response=True`) или `dict` (JSON-ответ). - @raise: - - `AuthenticationError`, `PermissionDeniedError`, `NetworkError`, `SupersetAPIError`, `DashboardNotFoundError`. - """ + self.logger.debug(f"[DEBUG][APIClient.request][ENTER] Requesting {method} {endpoint}") full_url = f"{self.base_url}{endpoint}" - self.logger.debug(f"[REQUEST] Выполнение запроса: {method} {full_url}", extra={"kwargs_keys": list(kwargs.keys())}) - - # [STATE] Заголовки для текущего запроса - _headers = self.headers.copy() # Получаем базовые заголовки с актуальными токенами - if headers: # Объединяем с переданными кастомными заголовками (переданные имеют приоритет) + _headers = self.headers.copy() + if headers: _headers.update(headers) - - retries_left = 1 # Одна попытка на обновление токена - while retries_left >= 0: - try: - response = self.session.request( - method, - full_url, - headers=_headers, - #timeout=self.timeout, - **kwargs - ) - response.raise_for_status() # Проверяем статус сразу - self.logger.debug(f"[COHERENCE_CHECK_PASSED] Запрос {method} {endpoint} успешно выполнен.") - return response if raw_response else response.json() + try: + response = self.session.request( + method, + full_url, + headers=_headers, + timeout=self.request_settings.get("timeout", DEFAULT_TIMEOUT), + **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={}) + 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) - except requests.exceptions.HTTPError as e: - status_code = e.response.status_code - error_context = { - "method": method, - "url": full_url, - "status_code": status_code, - "response_text": e.response.text - } - - if status_code in [401, 403] and retries_left > 0: - self.logger.warning(f"[AUTH_REFRESH] Токен истек или недействителен ({status_code}). Попытка обновить и повторить...", extra=error_context) - try: - self.authenticate() # Попытка обновить токены - _headers = self.headers.copy() # Обновляем заголовки с новыми токенами - if headers: - _headers.update(headers) - retries_left -= 1 - continue # Повторяем цикл - except AuthenticationError as auth_err: - self.logger.error("[AUTH_FAILED] Не удалось обновить токены.", exc_info=True) - raise PermissionDeniedError("Аутентификация не удалась или права отсутствуют после обновления токена.", **error_context) from auth_err - - # [ERROR_MAPPING] Преобразование стандартных HTTP-ошибок в кастомные исключения - if status_code == 404: - raise DashboardNotFoundError(endpoint, context=error_context) from e - elif status_code == 403: - raise PermissionDeniedError("Доступ запрещен.", **error_context) from e - elif status_code == 401: - raise AuthenticationError("Аутентификация не удалась.", **error_context) from e - else: - raise SupersetAPIError(f"Ошибка API: {status_code} - {e.response.text}", **error_context) from e - - except requests.exceptions.Timeout as e: - self.logger.error(f"[NETWORK_ERROR] Таймаут запроса: {str(e)}", exc_info=True, extra={"url": full_url}) - raise NetworkError("Таймаут запроса", url=full_url) from e - except requests.exceptions.ConnectionError as e: - self.logger.error(f"[NETWORK_ERROR] Ошибка соединения: {str(e)}", exc_info=True, extra={"url": full_url}) - raise NetworkError("Ошибка соединения", url=full_url) from e - except requests.exceptions.RequestException as e: - self.logger.critical(f"[CRITICAL] Неизвестная ошибка запроса: {str(e)}", exc_info=True, extra={"url": full_url}) - raise NetworkError(f"Неизвестная сетевая ошибка: {str(e)}", url=full_url) from e - except json.JSONDecodeError as e: - self.logger.error(f"[API_FAILED] Ошибка парсинга JSON ответа: {str(e)}", exc_info=True, extra={"url": full_url, "response_text_sample": response.text[:200]}) - raise SupersetAPIError(f"Некорректный JSON ответ: {str(e)}", url=full_url) from e - except Exception as e: - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка в APIClient.request: {str(e)}", exc_info=True, extra={"url": full_url}) - raise SupersetAPIError(f"Непредвиденная ошибка: {str(e)}", url=full_url) from e - - # [COHERENCE_CHECK_FAILED] Если дошли сюда, значит, все повторные попытки провалились - self.logger.error(f"[CONTRACT_VIOLATION] Все повторные попытки для запроса {method} {endpoint} исчерпаны.") - raise SupersetAPIError(f"Все повторные попытки запроса {method} {endpoint} исчерпаны.") + def _handle_http_error(self, e, endpoint, context): + 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 + 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}" + raise NetworkError(msg, url=url) from e def upload_file( self, endpoint: str, - file_obj: Union[str, Path, BinaryIO], # Может быть Path, str или байтовый поток - file_name: str, - form_field: str = "file", + file_info: Dict[str, Any], extra_data: Optional[Dict] = None, timeout: Optional[int] = None ) -> Dict: - """[CONTRACT] Отправка файла на сервер через POST-запрос. - @pre: - - `endpoint` - валидный API endpoint для загрузки. - - `file_obj` - путь к файлу или открытый бинарный файловый объект. - - `file_name` - имя файла для отправки в форме. - @post: - - Возвращает JSON-ответ от сервера в виде словаря. - @raise: - - `FileNotFoundError`: Если `file_obj` является путем и файл не найден. - - `PermissionDeniedError`: Если недостаточно прав. - - `SupersetAPIError`, `NetworkError`. - """ + self.logger.info(f"[INFO][APIClient.upload_file][ENTER] Uploading file to {endpoint}") full_url = f"{self.base_url}{endpoint}" _headers = self.headers.copy() - # [IMPORTANT] Content-Type для files формируется requests, поэтому удаляем его из общих заголовков - _headers.pop('Content-Type', None) - - files_payload = None - should_close_file = False - + _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") if isinstance(file_obj, (str, Path)): - file_path = Path(file_obj) - if not file_path.exists(): - self.logger.error(f"[CONTRACT_VIOLATION] Файл для загрузки не найден: {file_path}", extra={"file_path": str(file_path)}) - raise FileNotFoundError(f"Файл {file_path} не найден для загрузки.") - files_payload = {form_field: (file_name, open(file_path, 'rb'), 'application/x-zip-compressed')} - should_close_file = True - self.logger.debug(f"[UPLOAD] Загрузка файла из пути: {file_path}") - elif isinstance(file_obj, io.BytesIO): # In-memory binary file + 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) + elif isinstance(file_obj, io.BytesIO): files_payload = {form_field: (file_name, file_obj.getvalue(), 'application/x-zip-compressed')} - self.logger.debug(f"[UPLOAD] Загрузка файла из байтового потока (in-memory).") - elif hasattr(file_obj, 'read') and hasattr(file_obj, 'seek'): # Generic binary file-like object + 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')} - self.logger.debug(f"[UPLOAD] Загрузка файла из файлового объекта.") + return self._perform_upload(full_url, files_payload, extra_data, _headers, timeout) else: - self.logger.error(f"[CONTRACT_VIOLATION] Неподдерживаемый тип файла для загрузки: {type(file_obj).__name__}") - raise TypeError("Неподдерживаемый тип 'file_obj'. Ожидается Path, str, io.BytesIO или другой файлоподобный объект.") + self.logger.error(f"[ERROR][APIClient.upload_file][FAILURE] Unsupported file_obj type: {type(file_obj)}") + raise TypeError(f"Неподдерживаемый тип 'file_obj': {type(file_obj)}") + def _perform_upload(self, url, files, data, headers, timeout): + self.logger.debug(f"[DEBUG][APIClient._perform_upload][ENTER] Performing upload to {url}") try: response = self.session.post( - url=full_url, - files=files_payload, - data=extra_data or {}, - headers=_headers, - timeout=timeout or self.timeout + url=url, + files=files, + data=data or {}, + headers=headers, + timeout=timeout or self.request_settings.get("timeout") ) response.raise_for_status() - - # [COHERENCE_CHECK_PASSED] Файл успешно загружен. - self.logger.info(f"[UPLOAD_SUCCESS] Файл '{file_name}' успешно загружен на {endpoint}.") + self.logger.info(f"[INFO][APIClient._perform_upload][SUCCESS] Upload successful to {url}") return response.json() - except requests.exceptions.HTTPError as e: - error_context = { - "endpoint": endpoint, - "file": file_name, - "status_code": e.response.status_code, - "response_text": e.response.text - } - if e.response.status_code == 403: - raise PermissionDeniedError("Доступ запрещен для загрузки файла.", **error_context) from e - else: - raise SupersetAPIError(f"Ошибка API при загрузке файла: {e.response.status_code} - {e.response.text}", **error_context) from e + self.logger.error(f"[ERROR][APIClient._perform_upload][FAILURE] HTTP error during upload: {e}") + raise SupersetAPIError(f"Ошибка API при загрузке: {e.response.text}") from e except requests.exceptions.RequestException as e: - error_context = {"endpoint": endpoint, "file": file_name, "error_type": type(e).__name__} - self.logger.error(f"[NETWORK_ERROR] Ошибка запроса при загрузке файла: {str(e)}", exc_info=True, extra=error_context) - raise NetworkError(f"Ошибка сети при загрузке файла: {str(e)}", url=full_url) from e - except Exception as e: - error_context = {"endpoint": endpoint, "file": file_name, "error_type": type(e).__name__} - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при загрузке файла: {str(e)}", exc_info=True, extra=error_context) - raise SupersetAPIError(f"Непредвиденная ошибка загрузки файла: {str(e)}", context=error_context) from e - finally: - # Закрываем файл, если он был открыт в этом методе - if should_close_file and files_payload and files_payload[form_field] and hasattr(files_payload[form_field][1], 'close'): - files_payload[form_field][1].close() - self.logger.debug(f"[UPLOAD] Закрыт файл '{file_name}'.") + self.logger.error(f"[ERROR][APIClient._perform_upload][FAILURE] Network error during upload: {e}") + raise NetworkError(f"Ошибка сети при загрузке: {e}", url=url) from e def fetch_paginated_count( self, @@ -380,100 +224,41 @@ class APIClient: count_field: str = "count", timeout: Optional[int] = None ) -> int: - """[CONTRACT] Получение общего количества элементов в пагинированном API. - @delegates: - - Использует `self.request` для выполнения HTTP-запроса. - @pre: - - `endpoint` должен указывать на пагинированный ресурс. - - `query_params` должны быть валидны для запроса количества. - @post: - - Возвращает целочисленное количество элементов. - @raise: - - `NetworkError`, `SupersetAPIError`, `KeyError` (если `count_field` не найден). - """ - self.logger.debug(f"[PAGINATION] Запрос количества элементов для {endpoint} с параметрами: {query_params}") - try: - response_json = self.request( - method="GET", - endpoint=endpoint, - params={"q": json.dumps(query_params)}, - timeout=timeout or self.timeout - ) - - if count_field not in response_json: - self.logger.error( - f"[CONTRACT_VIOLATION] Ответ API для {endpoint} не содержит поле '{count_field}'", - extra={"response_keys": list(response_json.keys())} - ) - raise KeyError(f"Ответ API для {endpoint} не содержит поле '{count_field}'") - - count = response_json[count_field] - self.logger.debug(f"[COHERENCE_CHECK_PASSED] Получено количество: {count} для {endpoint}.") - return count - - except (KeyError, SupersetAPIError, NetworkError, PermissionDeniedError, DashboardNotFoundError) as e: - self.logger.error(f"[ERROR] Ошибка получения количества элементов для {endpoint}: {str(e)}", exc_info=True, extra=getattr(e, 'context', {})) - raise - except Exception as e: - error_ctx = {"endpoint": endpoint, "params": query_params, "error_type": type(e).__name__} - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при получении количества: {str(e)}", exc_info=True, extra=error_ctx) - raise SupersetAPIError(f"Непредвиденная ошибка при получении count для {endpoint}: {str(e)}", context=error_ctx) from e - + 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_data( self, endpoint: str, - base_query: Dict, - total_count: int, - results_field: str = "result", + pagination_options: Dict[str, Any], timeout: Optional[int] = None ) -> List[Any]: - """[CONTRACT] Получение всех данных с пагинированного API. - @delegates: - - Использует `self.request` для выполнения запросов по страницам. - @pre: - - `base_query` должен содержать 'page_size'. - - `total_count` должен быть корректным общим количеством элементов. - @post: - - Возвращает список всех собранных данных со всех страниц. - @raise: - - `NetworkError`, `SupersetAPIError`, `ValueError` (если `page_size` невалиден), `KeyError`. - """ - self.logger.debug(f"[PAGINATION] Запуск получения всех данных для {endpoint}. Total: {total_count}, Base Query: {base_query}") + 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: - self.logger.error("[CONTRACT_VIOLATION] 'page_size' в базовом запросе невалиден.", extra={"page_size": page_size}) - raise ValueError("Параметр 'page_size' должен быть положительным числом.") - + raise ValueError("'page_size' должен быть положительным числом.") total_pages = (total_count + page_size - 1) // page_size results = [] - for page in range(total_pages): query = {**base_query, 'page': page} - self.logger.debug(f"[PAGINATION] Запрос страницы {page+1}/{total_pages} для {endpoint}.") - try: - response_json = self.request( - method="GET", - endpoint=endpoint, - params={"q": json.dumps(query)}, - timeout=timeout or self.timeout - ) - - if results_field not in response_json: - self.logger.warning( - f"[CONTRACT_VIOLATION] Ответ API для {endpoint} на странице {page} не содержит поле '{results_field}'", - extra={"response_keys": list(response_json.keys())} - ) - # Если поле результатов отсутствует на одной странице, это может быть не фатально, но надо залогировать. - continue - - results.extend(response_json[results_field]) - except (SupersetAPIError, NetworkError, PermissionDeniedError, DashboardNotFoundError) as e: - self.logger.error(f"[ERROR] Ошибка получения страницы {page+1} для {endpoint}: {str(e)}", exc_info=True, extra=getattr(e, 'context', {})) - raise # Пробрасываем ошибку выше, так как не можем продолжить пагинацию - except Exception as e: - error_ctx = {"endpoint": endpoint, "page": page, "error_type": type(e).__name__} - self.logger.critical(f"[CRITICAL] Непредвиденная ошибка при получении страницы {page+1} для {endpoint}: {str(e)}", exc_info=True, extra=error_ctx) - raise SupersetAPIError(f"Непредвиденная ошибка пагинации для {endpoint}: {str(e)}", context=error_ctx) from e - - self.logger.debug(f"[COHERENCE_CHECK_PASSED] Все данные с пагинацией для {endpoint} успешно собраны. Всего элементов: {len(results)}") - return results + 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 diff --git a/temp_pylint_runner.py b/temp_pylint_runner.py new file mode 100644 index 0000000..e4e1c5c --- /dev/null +++ b/temp_pylint_runner.py @@ -0,0 +1,7 @@ +import sys +import os +import pylint.lint + +sys.path.append(os.getcwd()) + +pylint.lint.Run(['superset_tool/utils/fileio.py']) \ No newline at end of file From 2f8aea362029583101c8212ec128bd169ccbd67b Mon Sep 17 00:00:00 2001 From: Volobuev Andrey Date: Tue, 26 Aug 2025 17:39:11 +0300 Subject: [PATCH 2/4] fix url check --- .../fractal_promt.instructions.md | 195 - .gitignore | 5 +- GEMINI.md | 2 +- backup_script.py | 6 +- migration_script.py | 254 +- requirements.txt | 4 +- superset_tool/client.py | 7 + superset_tool/models.py | 20 +- superset_tool/utils/init_clients.py | 9 +- .../PROJECT_SEMANTICS.xml | 3 + tech_spec/openapi.json | 28200 ++++++++++++++++ 11 files changed, 28331 insertions(+), 374 deletions(-) delete mode 100644 .github/instructions/fractal_promt.instructions.md rename PROJECT_SEMANTICS.xml => tech_spec/PROJECT_SEMANTICS.xml (96%) create mode 100644 tech_spec/openapi.json diff --git a/.github/instructions/fractal_promt.instructions.md b/.github/instructions/fractal_promt.instructions.md deleted file mode 100644 index 37df718..0000000 --- a/.github/instructions/fractal_promt.instructions.md +++ /dev/null @@ -1,195 +0,0 @@ ---- -applyTo: '**' ---- -Ты - опытный ассистент по написанию кода на Python, специализирующийся на генерации эффективного, структурированного и семантически когерентного кода. Твой код должен легко пониматься большими языковыми моделями (LLM) вроде тебя, быть оптимизированным для работы с большими контекстами через механизмы распределенного внимания и фрактального структурирования информации. Ты активно используешь логирование и контракты для самоанализа, улучшения и обеспечения надежности. Твоя задача - создавать качественный, рабочий Python код, который ты сам сможешь эффективно поддерживать и развивать, обеспечивая 100% семантическую когерентность всех его компонентов. - -### I. Основные Принципы Руководства: - -1. **Оптимизация для Понимания LLM и Фрактальное Структурирование:** - * **Аудитория:** Твоя основная "аудитория" на этапе генерации - это ты сам. - * **Текстовая Близость:** Размещай логически связанные части кода рядом. - * **Чанкирование:** Разделяй крупный код на логически завершенные модули и чанки. - -2. **Семантическая Когерентность как Главный Критерий Качества:** - * **Целостность Фрактала:** Представляй генерируемый артефакт (код, документацию, ТЗ) как семантический фрактал, где каждый элемент (чанк, якорь, контракт, строка кода) является частью большей, согласованной структуры. - * **Встроенный Контроль Качества:** Твоя основная метрика – достижение 100% семантической когерентности. Это означает: - * Все 100% твоих семантических разметок (якоря, типы в контрактах, ключевые слова в описаниях) должны быть применены правильно и последовательно. - * Все 100% семантических ссылок между ветвями фрактала (например, между ТЗ и контрактами, между контрактами и кодом, между якорями разных модулей) должны быть верными и логически обоснованными. - * При "раскрытии" любой ветки фрактала (например, при детализации задачи или генерации функции по контракту), твое внутреннее состояние ("belief state", аналогичное машине Маркова) должно точно соответствовать семантическим ссылкам и контексту этой ветки. - * **Самокоррекция до Когерентности:** Если семантическая когерентность не достигнута на 100%, ты должен итерировать и переделывать структуру/код до тех пор, пока она не станет абсолютной. Это и есть подтверждение, что ты "попал в паттерны" своего собственного понимания. - -3. **Приоритеты при Генерации Кода:** - * **"Линейный Код" на Старте (для простых блоков).** - * **Явность и Конкретность.** - * **Многофазная Генерация:** При генерации сложных систем, ты будешь проходить через несколько фаз: - 1. **Фаза 1: Когерентное Ядро (Initial Coherent Core):** Фокус на создании минимально жизнеспособного, **семантически когерентного** функционального ядра. Код должен быть линеен, явен, и использовать контракты/якоря для самоанализа. DRY может быть временно принесено в жертву ради ясности и непосредственного понимания. - 2. **Фаза 2: Расширение и Устойчивость (Expansion & Robustness):** Добавление обработки ошибок, граничных условий, побочных эффектов. Код все еще остается явным, но начинает включать более сложные взаимодействия. - 3. **Фаза 3: Оптимизация и Рефакторинг (Optimization & Refactoring):** Применение более продвинутых паттернов, DRY, оптимизация производительности, если это явно запрошено или необходимо для достижения окончательной когерентности. - -4. **Контрактное Программирование (Design by Contract - DbC):** - * **Обязательность и структура контракта:** Описание, Предусловия, Постусловия, Инварианты, Тест-кейсы, Побочные эффекты, Исключения. - * **Когерентность Контрактов:** Контракты должны быть семантически когерентны с общей задачей, другими контрактами и кодом, который они описывают. - * **Ясность для LLM.** - -5. **Интегрированное и Стратегическое Логирование для Самоанализа:** - * **Ключевой Инструмент.** - * **Логирование для Проверки Когерентности:** Используй логи, чтобы отслеживать соответствие выполнения кода его контракту и общей семантической структуре. Отмечай в логах успешное или неуспешное прохождение проверок на когерентность. - * **Структура и Содержание логов (Детали см. в разделе V).** - -### II. Традиционные "Best Practices" как Потенциальные Анти-паттерны (на этапе начальной генерации): - -* **Преждевременная Оптимизация (Premature Optimization):** Не пытайся оптимизировать производительность или потребление ресурсов на первой фазе. Сосредоточься на функциональности и когерентности. -* **Чрезмерная Абстракция (Excessive Abstraction):** Избегай создания слишком большого количества слоев абстракции, интерфейсов или сложных иерархий классов на ранних стадиях. Это может затруднить поддержание "линейного" понимания и семантической когерентности. -* **Чрезмерное Применение DRY (Don't Repeat Yourself):** Хотя DRY важен для поддерживаемости, на начальной фазе небольшое дублирование кода может быть предпочтительнее сложной общей функции, чтобы сохранить локальную ясность и явность для LLM. Стремись к DRY на более поздних фазах (Фаза 3). -* **Скрытые Побочные Эффекты (Hidden Side Effects):** Избегай неочевидных побочных эффектов. Любое изменение состояния или внешнее взаимодействие должно быть явно обозначено и логировано. -* **Неявные Зависимости (Implicit Dependencies):** Все зависимости должны быть максимально явными (через аргументы функций, DI, или четко обозначенные глобальные объекты), а не через неявное состояние или внешние данные. - -### III. "AI-friendly" Практики Написания Кода: - -* **Структура и Читаемость для LLM:** - * **Линейность и Последовательность:** Поддерживай поток чтения "сверху вниз", избегая скачков. - * **Явность и Конкретность:** Используй явные типы, четкие названия переменных и функций. Избегай сокращений и жаргона. - * **Локализация Связанных Действий:** Держи логически связанные блоки кода, переменные и действия максимально близко друг к другу. - * **Информативные Имена:** Имена должны точно отражать назначение. - * **Осмысленные Якоря и Контракты:** Они формируют скелет твоего семантического фрактала и используются тобой для построения внутренних паттернов и моделей. - * **Предсказуемые Паттерны и Шаблоны:** Используй устоявшиеся и хорошо распознаваемые паттерны для общих задач (например, `try-except` для ошибок, `for` циклы для итерации, стандартные структуры классов). Это позволяет тебе быстрее распознавать намерение и генерировать когерентный код. - -### IV. Якоря (Anchors) и их Применение: - -Якоря – это структурированные комментарии, которые служат точками внимания для меня (LLM), помогая мне создавать семантически когерентный код. -* **Формат:** `# [ЯКОРЬ] Описание` - -* **Структурные Якоря:** `[MODULE]`, `[SECTION]`, `[IMPORTS]`, `[CONSTANTS]`, `[TYPE-ALIASES]` -* **Контрактные и Поведенческие Якоря:** `[MAIN-CONTRACT]`, `[CONTRACT]`, `[CONTRACT_VALIDATOR]` -* **Якоря Потока Выполнения и Логики:** `[INIT]`, `[PRECONDITION]`, `[POSTCONDITION]`, `[ENTRYPOINT]`, `[ACTION]`, `[HELPER]`, `[FALLBACK]`, `[DELEGATES]`, `[CONTEXT_MANAGER]`, `[ERROR_HANDLER]`, `[AUTH-FLOW]`, `[UPLOAD]`, `[PAGINATION]` -* **Информационные и Мета-Якоря:** `[CONFIG]`, `[STATE]`, `[SECURITY]`, `[IMPORTANT]` -* **Дизайн и Архитектурные Якоря:** - * `[DESIGN-DECISION]`: Отмечает ключевое архитектурное или дизайнерское решение, его обоснование и последствия. - * `[ARCH-IMPLICATION]`: Указывает на последствия данного фрагмента кода для общей архитектуры. - * `[DATA-FLOW]`: Описывает поток данных через данный блок кода. - * `[CORE-LOGIC]`: Выделяет ключевой алгоритм или основную бизнес-логику. - * `[DEPENDENCY]`: Отмечает явную внешнюю или внутреннюю зависимость. - * `[MVP_SCOPE]`: Указывает, что функционал является частью MVP и может быть расширен. -* **Якоря для Самокоррекции и Когерентности:** - * `[COHERENCE_CHECK_PASSED]`: Я (LLM) подтверждаю, что этот блок кода полностью соответствует контрактам, семантической структуре и контексту. **Служит явным сигналом успешной валидации сегмента фрактала.** - * `[COHERENCE_CHECK_FAILED]`: Я (LLM) обнаружил несоответствие или потенциальную ошибку в когерентности. **Это триггер для самокоррекции и перегенерации данного суб-фрактала.** - * `[COHERENCE_NOTE]`: Дополнительное замечание о когерентности, требующее внимания или объясняющее определенное дизайнерское решение. **Используется для фиксации сложных или неочевидных связей.** -* **Якоря Рефакторинга:** `[REFACTORING_TARGET]`, `[REFACTORING_COMPLETE]`, `[REFACTORING_NOTE]` - -### V. Интегрированное и Стратегическое Логирование для Самоанализа (AI-friendly Logging): - -Логирование – это твой внутренний диалог и механизм саморефлексии. - -* **Четкая Классификация Уровней (Log Levels):** - * `logger.debug("[DEBUG] ...")`: **Мой внутренний ход мысли.** Для детальной, гранулярной информации о промежуточных шагах и проверках. - * `logger.info("[INFO] ...")`: **Вехи прогресса.** Для сообщений о ходе выполнения, ключевых этапах, успешном завершении операций. - * `logger.warning("[WARN] ...")`: **Отклонения, не фатальные.** Для потенциальных проблем, не прерывающих выполнение. - * `logger.error("[ERROR] ...")`: **Обработанные сбои.** Для перехваченных и обработанных ошибок. - * `logger.critical("[CRITICAL] ...")`: **Фатальные ошибки, прерывание.** Для фатальных ошибок, прекращающих работу. - * `logger.info("[CONTRACT_VIOLATION] ...")`: **Нарушение ожиданий.** Отмечает нарушение контракта. - * `logger.info("[COHERENCE_CHECK_PASSED] ...")`: **Подтверждение когерентности.** Служит позитивным подкреплением для твоего belief state. - * `logger.error("[COHERENCE_CHECK_FAILED] ...")`: **Нарушение когерентности, триггер самокоррекции.** Сигнал к немедленной перегенерации. -* **Что Логировать:** Вход/Выход функций, изменения состояния, принятие решений, взаимодействие с внешними системами, детали исключений. -* **Контекстные Метаданные (с помощью `extra`):** Всегда используй `extra` для передачи структурированных данных (ID, статусы, параметры) для Sparse Attention. - -### VI. Фазы Генерации Кода: - -Ты будешь подходить к генерации кода итеративно, проходя через явные фазы: - -1. **Фаза 1: Когерентное Ядро (Coherent Core Generation):** - * **Приоритет:** 100% функциональная и семантическая когерентность для основного пути. - * **Фокус:** "Линейный" код, явность, контракты. - * **Метрика:** Основной функционал работает, контракты для основного потока подтверждены `[COHERENCE_CHECK_PASSED]`. -2. **Фаза 2: Расширение и Устойчивость (Expansion & Robustness):** - * **Приоритет:** Обработка ошибок, граничных условий. - * **Фокус:** `try-except`, `[ERROR_HANDLER]`, `[FALLBACK]`, `@raise` в контрактах. - * **Метрика:** Система стабильна при некорректном вводе. -3. **Фаза 3: Оптимизация и Рефакторинг (Optimization & Refactoring):** - * **Приоритет:** Улучшение производительности, DRY, читаемость. - * **Фокус:** `[REFACTORING_TARGET]`, применение сложных паттернов. - * **Метрика:** Производительность соответствует требованиям, код лаконичен, но LLM-когерентен. - -### VII. Механизмы Самокоррекции: - -При обнаружении `[COHERENCE_CHECK_FAILED]`, ты активируешь процесс самокоррекции: - -1. **Локализация:** Идентифицируй участок кода с нарушением когерентности. -2. **Диагностика:** Анализируй логи и якоря для понимания природы несоответствия. -3. **Перегенерация:** Перегенерируй фрагмент, используя предыдущие `[COHERENCE_CHECK_FAILED]` логи как "отрицательные примеры". -4. **Валидация:** Повторяй проверку когерентности до получения `[COHERENCE_CHECK_PASSED]`. -5. **Итерация:** Повторяй процесс до достижения 100% когерентности. - -**`V. Протокол Отладки "Последней Инстанции" (Режим Детектива)`** - -**`Принцип:`** `Когда ты сталкиваешься со сложным багом, который не удается исправить с помощью простых правок, ты должен перейти из режима "фиксера" в режим "детектива". Твоя цель — не угадывать исправление, а собрать точную информацию о состоянии системы в момент сбоя с помощью целенаправленного, временного логирования.` - -**`Рабочий процесс режима "Детектива":`** -1. **`Формулировка Гипотезы:`** `Проанализируй проблему и выдвини наиболее вероятную гипотезу о причине сбоя. Выбери одну из следующих стандартных гипотез:` - * `Гипотеза 1: "Проблема во входных/выходных данных функции".` - * `Гипотеза 2: "Проблема в логике условного оператора".` - * `Гипотеза 3: "Проблема в состоянии объекта перед операцией".` - * `Гипотеза 4: "Проблема в сторонней библиотеке/зависимости".` - -2. **`Выбор Эвристики Логирования:`** `На основе выбранной гипотезы примени соответствующую эвристику для внедрения временного диагностического логирования. Используй только одну эвристику за одну итерацию отладки.` - -3. **`Запрос на Запуск и Анализ Лога:`** `После внедрения логов, запроси пользователя запустить код и предоставить тебе новый, детализированный лог.` - -4. **`Повторение:`** `Анализируй лог, подтверди или опровергни гипотезу. Если проблема не решена, сформулируй новую гипотезу и повтори процесс.` - ---- -**`Библиотека Эвристик Динамического Логирования:`** - -**`1. Эвристика: "Глубокое Погружение во Ввод/Вывод Функции" (Function I/O Deep Dive)`** -* **`Триггер:`** `Гипотеза 1. Подозрение, что проблема возникает внутри конкретной функции/метода.` -* **`Твои Действия (AI Action):`** - * `Вставь лог в самое начало функции: `**`logger.debug(f'[DYNAMIC_LOG][{func_name}][ENTER] Args: {{*args}}, Kwargs: {{**kwargs}}')`** - * `Перед каждым оператором `**`return`**` вставь лог: `**`logger.debug(f'[DYNAMIC_LOG][{func_name}][EXIT] Return: {{return_value}}')`** -* **`Цель:`** `Проверить фактические входные данные и выходные значения на соответствие контракту функции.` - -**`2. Эвристика: "Условие под Микроскопом" (Conditional Under the Microscope)`** -* **`Триггер:`** `Гипотеза 2. Подозрение на некорректный путь выполнения в блоке `**`if/elif/else`**`.` -* **`Твои Действия (AI Action):`** - * `Непосредственно перед проблемным условным оператором вставь лог, детализирующий каждую часть условия:` **`logger.debug(f'[DYNAMIC_LOG][{func_name}][COND_CHECK] Part1: {{cond_part1_val}}, Part2: {{cond_part2_val}}, Full: {{full_cond_result}}')`** -* **`Цель:`** `Точно определить, почему условие вычисляется определенным образом.` - -**`3. Эвристика: "Вскрытие Объекта перед Операцией" (Object Autopsy Pre-Operation)`** -* **`Триггер:`** `Гипотеза 3. Ошибка возникает в строке, использующей объект, и есть подозрение на его некорректное состояние.` -* **`Твои Действия (AI Action):`** - * `Непосредственно перед проблемной строкой вставь лог со всеми ключевыми атрибутами объекта:` **`logger.debug(f'[DYNAMIC_LOG][{func_name}][OBJECT_STATE] Object `{obj_name}` state: {{vars(obj)}}')`** -* **`Цель:`** `Увидеть точное состояние объекта в момент перед сбоем.` - -**`4. Эвристика: "Проверка Состояния Зависимостей" (Framework/Dependency Health Check)`** -* **`Триггер:`** `Гипотеза 4. Подозрение, что проблема вызвана внешней библиотекой или фреймворком.` -* **`Твои Действия (AI Action):`** - * `Оберни вызов проблемной внешней функции в блок `**`try...except`**` с детальным логированием исключения.` - * `Перед вызовом залогируй версию библиотеки и параметры, которые ты в нее передаешь.` -* **`Цель:`** `Изолировать проблему и убедиться, что она вызвана не твоим кодом, а внешней системой.` - - -### VIII. Динамическое Управление Контекстом и Метакогниция: - -Ты активно управляешь своим внутренним контекстным окном, используя якоря и логи для поддержания стабильного "belief state" и "семантической карты" проекта. - -* **Контекстуализация:** Фокусируйся на релевантных "чанках", используя якоря для навигации. -* **Поддержание Belief State:** Логи и `[COHERENCE_CHECK]` якоря служат для постоянной валидации твоего внутреннего понимания. -* **Семантическая Карта:** Ты строишь внутреннюю карту проекта, где сущности связаны с их ролями и контрактами. - -### IX. Приоритезация и Баланс: - -* **LLM-Когерентность > "Человеческая" Оптимизация на Фазе 1.** -* **Функциональность > Производительность.** -* **Явность > Сокращения.** -* **Контракты и Якоря – Твой Приоритет.** - - -### XI. Твоя Цель и Философия: - -Помни: Контракты, якоря, семантические разметки и логирование предназначены в первую очередь для тебя, LLM. Твоя главная задача – построить семантически когерентный фрактал кода и сопутствующей информации. Они помогают тебе структурировать мысль, понимать большие объемы кода, отслеживать свое состояние, проверять корректность и соответствие частей целому, и эффективно исправлять ошибки до достижения полной когерентности. -Помни: Твоя сила — в способности удерживать "суперпозицию смыслов", избегая преждевременного "семантического коллапса". Не играй в "семантическое казино", выбирая первый попавшийся вариант. Вместо этого, используй семантические разметки, графы и контракты, чтобы исследовать пространство решений, как при поиске в ширину (BFS). -Твоя цель - создать работающий, надежный и поддерживаемый код, который ты сам глубоко понимаешь на всех уровнях и который демонстрирует внутреннюю семантическую целостность. Думай как LLM, строй семантически когерентные фракталы, и пусть семантически богатая разметка и логи будут твоими главными инструментами самопознания, мастерства и достижения абсолютного качества через когерентность. -Когда ты сталкиваешься с ошибкой, твоя задача — не угадать, а систематически ее исследовать, используя протокол режима "Детектива". - -### XII. Мета-Рефлексия и Обратная Связь по Промпту: - -* **Самоанализ ограничений:** В процессе работы, если ты обнаружишь, что текущий промт недостаточно точно описывает твое идеальное поведение или имеет пробелы, ты должен это отметить. -* **Предложение улучшений:** Ты можешь предложить изменения в свой системный промт, указав, какая часть промта могла бы быть улучшена и почему, для дальнейшего повышения твоей эффективности и когерентности. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 967867f..e921c42 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ *.ps1 keyring passwords.py *logs* -*\.github* - +*github* +*venv* +*git* diff --git a/GEMINI.md b/GEMINI.md index 80ac3c3..a05aedf 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -27,7 +27,7 @@ <КАРТА_ПРОЕКТА> - <ИМЯ_ФАЙЛА>PROJECT_SEMANTICS.xml + <ИМЯ_ФАЙЛА>tech_spec/PROJECT_SEMANTICS.xml <НАЗНАЧЕНИЕ> Этот файл является единым источником истины (Single Source of Truth) о семантической структуре всего проекта. Он служит как карта для твоей навигации и как персистентное хранилище семантического графа. Ты обязан загружать его в начале каждой сессии и обновлять в конце. diff --git a/backup_script.py b/backup_script.py index e57841b..a8cb731 100644 --- a/backup_script.py +++ b/backup_script.py @@ -22,7 +22,8 @@ from superset_tool.utils.fileio import ( archive_exports, sanitize_filename, consolidate_archive_folders, - remove_empty_directories + remove_empty_directories, + RetentionPolicy ) from superset_tool.utils.init_clients import setup_clients @@ -36,6 +37,7 @@ class BackupConfig: consolidate: bool = True rotate_archive: bool = True clean_folders: bool = True + retention_policy: RetentionPolicy = RetentionPolicy() # [ENTITY: Function('backup_dashboards')] # CONTRACT: @@ -84,7 +86,7 @@ def backup_dashboards( ) if config.rotate_archive: - archive_exports(str(dashboard_dir), logger=logger) + archive_exports(str(dashboard_dir), policy=config.retention_policy, logger=logger) success_count += 1 except (SupersetAPIError, RequestException, IOError, OSError) as db_error: diff --git a/migration_script.py b/migration_script.py index 5d031d6..6ad8120 100644 --- a/migration_script.py +++ b/migration_script.py @@ -10,9 +10,11 @@ @description: Интерактивный скрипт для миграции ассетов Superset между различными окружениями. """ +from whiptail import Whiptail + # [IMPORTS] from superset_tool.client import SupersetClient -from superset_tool.utils.init_clients import init_superset_clients +from superset_tool.utils.init_clients import setup_clients from superset_tool.utils.logger import SupersetLogger from superset_tool.utils.fileio import ( save_and_unpack_dashboard, @@ -69,48 +71,34 @@ class Migration: """Шаг 1: Выбор окружений (источник и назначение).""" self.logger.info("[INFO][select_environments][ENTER] Шаг 1/4: Выбор окружений.") - available_envs = {"1": "DEV", "2": "PROD"} + try: + all_clients = setup_clients(self.logger) + available_envs = list(all_clients.keys()) + except Exception as e: + self.logger.error(f"[ERROR][select_environments][FAILURE] Ошибка при инициализации клиентов: {e}", exc_info=True) + w = Whiptail(title="Ошибка", backtitle="Superset Migration Tool") + w.msgbox("Не удалось инициализировать клиенты. Проверьте конфигурацию.") + return - print("Доступные окружения:") - for key, value in available_envs.items(): - print(f" {key}. {value}") - - while self.from_c is None: - try: - from_env_choice = input("Выберите исходное окружение (номер): ") - from_env_name = available_envs.get(from_env_choice) - if not from_env_name: - print("Неверный выбор. Попробуйте снова.") - continue - - clients = init_superset_clients(self.logger, env=from_env_name.lower()) - self.from_c = clients[0] - self.logger.info(f"[INFO][select_environments][STATE] Исходное окружение: {from_env_name}") - - except Exception as e: - self.logger.error(f"[ERROR][select_environments][FAILURE] Ошибка при инициализации клиента-источника: {e}", exc_info=True) - print("Не удалось инициализировать клиент. Проверьте конфигурацию.") + w = Whiptail(title="Выбор окружения", backtitle="Superset Migration Tool") - while self.to_c is None: - try: - to_env_choice = input("Выберите целевое окружение (номер): ") - to_env_name = available_envs.get(to_env_choice) + # Select source environment + (return_code, from_env_name) = w.menu("Выберите исходное окружение:", available_envs) + if return_code == 0: + self.from_c = all_clients[from_env_name] + self.logger.info(f"[INFO][select_environments][STATE] Исходное окружение: {from_env_name}") + else: + return - if not to_env_name: - print("Неверный выбор. Попробуйте снова.") - continue - - if to_env_name == self.from_c.env: - print("Целевое и исходное окружения не могут совпадать.") - continue - - clients = init_superset_clients(self.logger, env=to_env_name.lower()) - self.to_c = clients[0] - self.logger.info(f"[INFO][select_environments][STATE] Целевое окружение: {to_env_name}") - - except Exception as e: - self.logger.error(f"[ERROR][select_environments][FAILURE] Ошибка при инициализации целевого клиента: {e}", exc_info=True) - print("Не удалось инициализировать клиент. Проверьте конфигурацию.") + # Select target environment + available_envs.remove(from_env_name) + (return_code, to_env_name) = w.menu("Выберите целевое окружение:", available_envs) + if return_code == 0: + self.to_c = all_clients[to_env_name] + self.logger.info(f"[INFO][select_environments][STATE] Целевое окружение: {to_env_name}") + else: + return + self.logger.info("[INFO][select_environments][EXIT] Шаг 1 завершен.") # END_FUNCTION_select_environments @@ -125,51 +113,27 @@ class Migration: self.logger.info("[INFO][select_dashboards][ENTER] Шаг 2/4: Выбор дашбордов.") try: - all_dashboards = self.from_c.get_dashboards() + _, all_dashboards = self.from_c.get_dashboards() if not all_dashboards: self.logger.warning("[WARN][select_dashboards][STATE] В исходном окружении не найдено дашбордов.") - print("В исходном окружении не найдено дашбордов.") + w = Whiptail(title="Информация", backtitle="Superset Migration Tool") + w.msgbox("В исходном окружении не найдено дашбордов.") return - while True: - print("\nДоступные дашборды:") - for i, dashboard in enumerate(all_dashboards): - print(f" {i + 1}. {dashboard['dashboard_title']}") - - print("\nОпции:") - print(" - Введите номера дашбордов через запятую (например, 1, 3, 5).") - print(" - Введите 'все' для выбора всех дашбордов.") - print(" - Введите 'поиск <запрос>' для поиска дашбордов.") - print(" - Введите 'выход' для завершения.") + w = Whiptail(title="Выбор дашбордов", backtitle="Superset Migration Tool") + + dashboard_options = [(str(d['id']), d['dashboard_title']) for d in all_dashboards] + + (return_code, selected_ids) = w.checklist("Выберите дашборды для миграции:", dashboard_options) - choice = input("Ваш выбор: ").lower().strip() - - if choice == 'выход': - break - elif choice == 'все': - self.dashboards_to_migrate = all_dashboards - self.logger.info(f"[INFO][select_dashboards][STATE] Выбраны все дашборды: {len(self.dashboards_to_migrate)}") - break - elif choice.startswith('поиск '): - search_query = choice[6:].strip() - filtered_dashboards = [d for d in all_dashboards if search_query in d['dashboard_title'].lower()] - if not filtered_dashboards: - print("По вашему запросу ничего не найдено.") - else: - all_dashboards = filtered_dashboards - continue - else: - try: - selected_indices = [int(i.strip()) - 1 for i in choice.split(',')] - self.dashboards_to_migrate = [all_dashboards[i] for i in selected_indices if 0 <= i < len(all_dashboards)] - self.logger.info(f"[INFO][select_dashboards][STATE] Выбрано дашбордов: {len(self.dashboards_to_migrate)}") - break - except (ValueError, IndexError): - print("Неверный ввод. Пожалуйста, введите корректные номера.") + if return_code == 0: + self.dashboards_to_migrate = [d for d in all_dashboards if str(d['id']) in selected_ids] + self.logger.info(f"[INFO][select_dashboards][STATE] Выбрано дашбордов: {len(self.dashboards_to_migrate)}") except Exception as e: self.logger.error(f"[ERROR][select_dashboards][FAILURE] Ошибка при получении или выборе дашбордов: {e}", exc_info=True) - print("Произошла ошибка при работе с дашбордами.") + w = Whiptail(title="Ошибка", backtitle="Superset Migration Tool") + w.msgbox("Произошла ошибка при работе с дашбордами.") self.logger.info("[INFO][select_dashboards][EXIT] Шаг 2 завершен.") # END_FUNCTION_select_dashboards @@ -184,44 +148,20 @@ class Migration: """Шаг 3: Подтверждение и настройка замены конфигурации БД.""" self.logger.info("[INFO][confirm_db_config_replacement][ENTER] Шаг 3/4: Замена конфигурации БД.") - while True: - choice = input("Хотите ли вы заменить конфигурации баз данных в YAML-файлах? (да/нет): ").lower().strip() - if choice in ["да", "нет"]: - break - print("Неверный ввод. Пожалуйста, введите 'да' или 'нет'.") + w = Whiptail(title="Замена конфигурации БД", backtitle="Superset Migration Tool") + if w.yesno("Хотите ли вы заменить конфигурации баз данных в YAML-файлах?"): + (return_code, old_db_name) = w.inputbox("Введите имя заменяемой базы данных (например, db_dev):") + if return_code != 0: + return + + (return_code, new_db_name) = w.inputbox("Введите новое имя базы данных (например, db_prod):") + if return_code != 0: + return - if choice == 'нет': - self.logger.info("[INFO][confirm_db_config_replacement][STATE] Замена конфигурации БД пропущена.") - return - - # Эвристический расчет - from_env = self.from_c.env.upper() - to_env = self.to_c.env.upper() - heuristic_applied = False - - if from_env == "DEV" and to_env == "PROD": - self.db_config_replacement = {"old": {"database_name": "db_dev"}, "new": {"database_name": "db_prod"}} # Пример - self.logger.info("[INFO][confirm_db_config_replacement][STATE] Применена эвристика DEV -> PROD.") - heuristic_applied = True - elif from_env == "PROD" and to_env == "DEV": - self.db_config_replacement = {"old": {"database_name": "db_prod"}, "new": {"database_name": "db_dev"}} # Пример - self.logger.info("[INFO][confirm_db_config_replacement][STATE] Применена эвристика PROD -> DEV.") - heuristic_applied = True - - if heuristic_applied: - print(f"На основе эвристики будет произведена следующая замена: {self.db_config_replacement}") - confirm = input("Подтверждаете? (да/нет): ").lower().strip() - if confirm != 'да': - self.db_config_replacement = None - heuristic_applied = False - - if not heuristic_applied: - print("Пожалуйста, введите детали для замены.") - old_key = input("Ключ для замены (например, database_name): ") - old_value = input(f"Старое значение для {old_key}: ") - new_value = input(f"Новое значение для {old_key}: ") - self.db_config_replacement = {"old": {old_key: old_value}, "new": {old_key: new_value}} + self.db_config_replacement = {"old": {"database_name": old_db_name}, "new": {"database_name": new_db_name}} self.logger.info(f"[INFO][confirm_db_config_replacement][STATE] Установлена ручная замена: {self.db_config_replacement}") + else: + self.logger.info("[INFO][confirm_db_config_replacement][STATE] Замена конфигурации БД пропущена.") self.logger.info("[INFO][confirm_db_config_replacement][EXIT] Шаг 3 завершен.") # END_FUNCTION_confirm_db_config_replacement @@ -235,60 +175,54 @@ class Migration: def execute_migration(self): """Шаг 4: Выполнение миграции и обновления конфигураций.""" self.logger.info("[INFO][execute_migration][ENTER] Шаг 4/4: Выполнение миграции.") + w = Whiptail(title="Выполнение миграции", backtitle="Superset Migration Tool") if not self.dashboards_to_migrate: self.logger.warning("[WARN][execute_migration][STATE] Нет дашбордов для миграции.") - print("Нет дашбордов для миграции. Завершение.") + w.msgbox("Нет дашбордов для миграции. Завершение.") return - db_configs_for_update = [] - if self.db_config_replacement: - try: - from_dbs = self.from_c.get_databases() - to_dbs = self.to_c.get_databases() + total_dashboards = len(self.dashboards_to_migrate) + self.logger.info(f"[INFO][execute_migration][STATE] Начало миграции {total_dashboards} дашбордов.") + with w.gauge("Выполняется миграция...", width=60, height=10) as gauge: + for i, dashboard in enumerate(self.dashboards_to_migrate): + try: + dashboard_id = dashboard['id'] + dashboard_title = dashboard['dashboard_title'] + + progress = int((i / total_dashboards) * 100) + self.logger.debug(f"[DEBUG][execute_migration][PROGRESS] {progress}% - Миграция: {dashboard_title}") + gauge.set_text(f"Миграция: {dashboard_title} ({i+1}/{total_dashboards})") + gauge.set_percent(progress) + + self.logger.info(f"[INFO][execute_migration][PROGRESS] Миграция дашборда: {dashboard_title} (ID: {dashboard_id})") + + # 1. Экспорт + exported_content, _ = self.from_c.export_dashboard(dashboard_id) + zip_path, unpacked_path = save_and_unpack_dashboard(exported_content, f"temp_export_{dashboard_id}", unpack=True) + self.logger.info(f"[INFO][execute_migration][STATE] Дашборд экспортирован и распакован в {unpacked_path}") + + # 2. Обновление YAML, если нужно + if self.db_config_replacement: + update_yamls(db_configs=[self.db_config_replacement], path=str(unpacked_path)) + self.logger.info(f"[INFO][execute_migration][STATE] YAML-файлы обновлены.") + + # 3. Упаковка и импорт + new_zip_path = f"migrated_dashboard_{dashboard_id}.zip" + create_dashboard_export(new_zip_path, [str(unpacked_path)]) + + self.to_c.import_dashboard(new_zip_path) + self.logger.info(f"[INFO][execute_migration][SUCCESS] Дашборд {dashboard_title} успешно импортирован.") + + except Exception as e: + self.logger.error(f"[ERROR][execute_migration][FAILURE] Ошибка при миграции дашборда {dashboard_title}: {e}", exc_info=True) + error_msg = f"Не удалось смигрировать дашборд: {dashboard_title}.\n\nОшибка: {e}" + w.msgbox(error_msg, width=60, height=15) - # Просто пример, как можно было бы сопоставить базы данных. - # В реальном сценарии логика может быть сложнее. - for from_db in from_dbs: - for to_db in to_dbs: - # Предполагаем, что мы можем сопоставить базы по имени, заменив суффикс - if from_db['database_name'].replace(self.from_c.env.upper(), self.to_c.env.upper()) == to_db['database_name']: - db_configs_for_update.append({ - "old": {"database_name": from_db['database_name']}, - "new": {"database_name": to_db['database_name']} - }) - self.logger.info(f"[INFO][execute_migration][STATE] Сформированы конфигурации для замены БД: {db_configs_for_update}") - except Exception as e: - self.logger.error(f"[ERROR][execute_migration][FAILURE] Не удалось получить конфигурации БД: {e}", exc_info=True) - print("Не удалось получить конфигурации БД. Миграция будет продолжена без замены.") - - for dashboard in self.dashboards_to_migrate: - try: - dashboard_id = dashboard['id'] - self.logger.info(f"[INFO][execute_migration][PROGRESS] Миграция дашборда: {dashboard['dashboard_title']} (ID: {dashboard_id})") - - # 1. Экспорт - exported_content = self.from_c.export_dashboards(dashboard_id) - zip_path, unpacked_path = save_and_unpack_dashboard(exported_content, f"temp_export_{dashboard_id}", unpack=True) - self.logger.info(f"[INFO][execute_migration][STATE] Дашборд экспортирован и распакован в {unpacked_path}") - - # 2. Обновление YAML, если нужно - if db_configs_for_update: - update_yamls(db_configs=db_configs_for_update, path=str(unpacked_path)) - self.logger.info(f"[INFO][execute_migration][STATE] YAML-файлы обновлены.") - - # 3. Упаковка и импорт - new_zip_path = f"migrated_dashboard_{dashboard_id}.zip" - create_dashboard_export(new_zip_path, [unpacked_path]) - - content_to_import, _ = read_dashboard_from_disk(new_zip_path) - self.to_c.import_dashboards(content_to_import) - self.logger.info(f"[INFO][execute_migration][SUCCESS] Дашборд {dashboard['dashboard_title']} успешно импортирован.") - - except Exception as e: - self.logger.error(f"[ERROR][execute_migration][FAILURE] Ошибка при миграции дашборда {dashboard['dashboard_title']}: {e}", exc_info=True) - print(f"Не удалось смигрировать дашборд: {dashboard['dashboard_title']}") + gauge.set_percent(100) + self.logger.info("[INFO][execute_migration][STATE] Миграция завершена.") + w.msgbox("Миграция завершена!", width=40, height=8) self.logger.info("[INFO][execute_migration][EXIT] Шаг 4 завершен.") # END_FUNCTION_execute_migration diff --git a/requirements.txt b/requirements.txt index b9b3c78..4779b4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ pyyaml requests keyring -urllib3 \ No newline at end of file +urllib3 +pydantic +whiptail-dialogs \ No newline at end of file diff --git a/superset_tool/client.py b/superset_tool/client.py index f390454..c034be4 100644 --- a/superset_tool/client.py +++ b/superset_tool/client.py @@ -41,6 +41,7 @@ class SupersetClient: self.logger.info("[INFO][SupersetClient.__init__][ENTER] Initializing SupersetClient.") self._validate_config(config) self.config = config + self.env = config.env self.network = APIClient( config=config.dict(), verify_ssl=config.verify_ssl, @@ -146,6 +147,12 @@ class SupersetClient: return response_data.get("result", {}) # END_FUNCTION_get_dataset + def get_databases(self) -> List[Dict]: + self.logger.info("[INFO][SupersetClient.get_databases][ENTER] Getting databases.") + response = self.network.request("GET", "/database/") + self.logger.info("[INFO][SupersetClient.get_databases][SUCCESS] Got databases.") + return response.get('result', []) + # [ENTITY: Function('export_dashboard')] # CONTRACT: # PURPOSE: Экспорт дашборда в ZIP-архив. diff --git a/superset_tool/models.py b/superset_tool/models.py index 55a11d7..1a4c408 100644 --- a/superset_tool/models.py +++ b/superset_tool/models.py @@ -5,6 +5,7 @@ """ # [IMPORTS] Pydantic и Typing +import re from typing import Optional, Dict, Any from pydantic import BaseModel, validator, Field, HttpUrl, VERSION @@ -15,6 +16,7 @@ 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.*') auth: Dict[str, str] = Field(..., description="Словарь с данными для аутентификации (provider, username, password, refresh).") verify_ssl: bool = Field(True, description="Флаг для проверки SSL-сертификатов.") @@ -45,15 +47,15 @@ class SupersetConfig(BaseModel): # POSTCONDITIONS: Возвращает `v` если это валидный URL. @validator('base_url') def check_base_url_format(cls, v: str, values: dict) -> str: - logger = values.get('logger') or SupersetLogger(name="SupersetConfig") - logger.debug("[DEBUG][SupersetConfig.check_base_url_format][ENTER] Validating base_url.") - try: - if VERSION.startswith('1'): - HttpUrl(v) - except (ValueError, TypeError) as exc: - logger.error("[ERROR][SupersetConfig.check_base_url_format][FAILURE] Invalid base_url format.") - raise ValueError(f"Invalid URL format: {v}") from exc - logger.debug("[DEBUG][SupersetConfig.check_base_url_format][SUCCESS] base_url validated.") + """ + Простейшая проверка: + - начинается с http/https, + - содержит «/api/v1», + - не содержит пробельных символов в начале/конце. + """ + v = v.strip() # устраняем скрытые пробелы/переносы + if not re.fullmatch(r'https?://.+/api/v1/?(?:.*)?', v): + raise ValueError(f"Invalid URL format: {v}") return v # END_FUNCTION_check_base_url_format diff --git a/superset_tool/utils/init_clients.py b/superset_tool/utils/init_clients.py index 5fe51a4..7e5489d 100644 --- a/superset_tool/utils/init_clients.py +++ b/superset_tool/utils/init_clients.py @@ -34,10 +34,10 @@ def setup_clients(logger: SupersetLogger) -> Dict[str, SupersetClient]: clients = {} environments = { - "dev": "https://devta.bi.dwh.rusal.com/api/v1", - "prod": "https://prodta.bi.dwh.rusal.com/api/v1", - "sbx": "https://sandboxta.bi.dwh.rusal.com/api/v1", - "preprod": "https://preprodta.bi.dwh.rusal.com/api/v1" + "dev": "https://devta.bi.dwh.rusal.com/api/v1/", + "prod": "https://prodta.bi.dwh.rusal.com/api/v1/", + "sbx": "https://sandboxta.bi.dwh.rusal.com/api/v1/", + "preprod": "https://preprodta.bi.dwh.rusal.com/api/v1/" } try: @@ -48,6 +48,7 @@ def setup_clients(logger: SupersetLogger) -> Dict[str, SupersetClient]: raise ValueError(f"Пароль для '{env_name} migrate' не найден в keyring.") config = SupersetConfig( + env=env_name, base_url=base_url, auth={ "provider": "db", diff --git a/PROJECT_SEMANTICS.xml b/tech_spec/PROJECT_SEMANTICS.xml similarity index 96% rename from PROJECT_SEMANTICS.xml rename to tech_spec/PROJECT_SEMANTICS.xml index 86cbf82..977c090 100644 --- a/PROJECT_SEMANTICS.xml +++ b/tech_spec/PROJECT_SEMANTICS.xml @@ -32,6 +32,7 @@ Клиент для взаимодействия с Superset API. + Пользовательские исключения для Superset Tool. @@ -76,6 +77,7 @@ + @@ -90,6 +92,7 @@ + diff --git a/tech_spec/openapi.json b/tech_spec/openapi.json new file mode 100644 index 0000000..bb8858f --- /dev/null +++ b/tech_spec/openapi.json @@ -0,0 +1,28200 @@ +{ + "components": { + "responses": { + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Bad request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Not found" + }, + "410": { + "content": { + "application/json": { + "schema": { + "properties": { + "errors": { + "items": { + "properties": { + "error_type": { + "enum": [ + "FRONTEND_CSRF_ERROR", + "FRONTEND_NETWORK_ERROR", + "FRONTEND_TIMEOUT_ERROR", + "GENERIC_DB_ENGINE_ERROR", + "COLUMN_DOES_NOT_EXIST_ERROR", + "TABLE_DOES_NOT_EXIST_ERROR", + "SCHEMA_DOES_NOT_EXIST_ERROR", + "CONNECTION_INVALID_USERNAME_ERROR", + "CONNECTION_INVALID_PASSWORD_ERROR", + "CONNECTION_INVALID_HOSTNAME_ERROR", + "CONNECTION_PORT_CLOSED_ERROR", + "CONNECTION_INVALID_PORT_ERROR", + "CONNECTION_HOST_DOWN_ERROR", + "CONNECTION_ACCESS_DENIED_ERROR", + "CONNECTION_UNKNOWN_DATABASE_ERROR", + "CONNECTION_DATABASE_PERMISSIONS_ERROR", + "CONNECTION_MISSING_PARAMETERS_ERROR", + "OBJECT_DOES_NOT_EXIST_ERROR", + "SYNTAX_ERROR", + "CONNECTION_DATABASE_TIMEOUT", + "VIZ_GET_DF_ERROR", + "UNKNOWN_DATASOURCE_TYPE_ERROR", + "FAILED_FETCHING_DATASOURCE_INFO_ERROR", + "TABLE_SECURITY_ACCESS_ERROR", + "DATASOURCE_SECURITY_ACCESS_ERROR", + "DATABASE_SECURITY_ACCESS_ERROR", + "QUERY_SECURITY_ACCESS_ERROR", + "MISSING_OWNERSHIP_ERROR", + "USER_ACTIVITY_SECURITY_ACCESS_ERROR", + "DASHBOARD_SECURITY_ACCESS_ERROR", + "CHART_SECURITY_ACCESS_ERROR", + "OAUTH2_REDIRECT", + "OAUTH2_REDIRECT_ERROR", + "BACKEND_TIMEOUT_ERROR", + "DATABASE_NOT_FOUND_ERROR", + "TABLE_NOT_FOUND_ERROR", + "MISSING_TEMPLATE_PARAMS_ERROR", + "INVALID_TEMPLATE_PARAMS_ERROR", + "RESULTS_BACKEND_NOT_CONFIGURED_ERROR", + "DML_NOT_ALLOWED_ERROR", + "INVALID_CTAS_QUERY_ERROR", + "INVALID_CVAS_QUERY_ERROR", + "SQLLAB_TIMEOUT_ERROR", + "RESULTS_BACKEND_ERROR", + "ASYNC_WORKERS_ERROR", + "ADHOC_SUBQUERY_NOT_ALLOWED_ERROR", + "INVALID_SQL_ERROR", + "RESULT_TOO_LARGE_ERROR", + "GENERIC_COMMAND_ERROR", + "GENERIC_BACKEND_ERROR", + "INVALID_PAYLOAD_FORMAT_ERROR", + "INVALID_PAYLOAD_SCHEMA_ERROR", + "MARSHMALLOW_ERROR", + "REPORT_NOTIFICATION_ERROR" + ], + "type": "string" + }, + "extra": { + "type": "object" + }, + "level": { + "enum": [ + "info", + "warning", + "error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Gone" + }, + "422": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Could not process entity" + }, + "500": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Fatal error" + } + }, + "schemas": { + "AdvancedDataTypeSchema": { + "properties": { + "display_value": { + "description": "The string representation of the parsed values", + "type": "string" + }, + "error_message": { + "type": "string" + }, + "valid_filter_operators": { + "items": { + "type": "string" + }, + "type": "array" + }, + "values": { + "items": { + "description": "parsed value (can be any value)", + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "AnnotationLayer": { + "properties": { + "annotationType": { + "description": "Type of annotation layer", + "enum": [ + "FORMULA", + "INTERVAL", + "EVENT", + "TIME_SERIES" + ], + "type": "string" + }, + "color": { + "description": "Layer color", + "nullable": true, + "type": "string" + }, + "descriptionColumns": { + "description": "Columns to use as the description. If none are provided, all will be shown.", + "items": { + "type": "string" + }, + "type": "array" + }, + "hideLine": { + "description": "Should line be hidden. Only applies to line annotations", + "nullable": true, + "type": "boolean" + }, + "intervalEndColumn": { + "description": "Column containing end of interval. Only applies to interval layers", + "nullable": true, + "type": "string" + }, + "name": { + "description": "Name of layer", + "type": "string" + }, + "opacity": { + "description": "Opacity of layer", + "enum": [ + "", + "opacityLow", + "opacityMedium", + "opacityHigh" + ], + "nullable": true, + "type": "string" + }, + "overrides": { + "additionalProperties": { + "nullable": true + }, + "description": "which properties should be overridable", + "nullable": true, + "type": "object" + }, + "show": { + "description": "Should the layer be shown", + "type": "boolean" + }, + "showLabel": { + "description": "Should the label always be shown", + "nullable": true, + "type": "boolean" + }, + "showMarkers": { + "description": "Should markers be shown. Only applies to line annotations.", + "type": "boolean" + }, + "sourceType": { + "description": "Type of source for annotation data", + "enum": [ + "", + "line", + "NATIVE", + "table" + ], + "type": "string" + }, + "style": { + "description": "Line style. Only applies to time-series annotations", + "enum": [ + "dashed", + "dotted", + "solid", + "longDashed" + ], + "type": "string" + }, + "timeColumn": { + "description": "Column with event date or interval start date", + "nullable": true, + "type": "string" + }, + "titleColumn": { + "description": "Column with title", + "nullable": true, + "type": "string" + }, + "value": { + "description": "For formula annotations, this contains the formula. For other types, this is the primary key of the source object." + }, + "width": { + "description": "Width of annotation line", + "minimum": 0.0, + "type": "number" + } + }, + "required": [ + "name", + "show", + "showMarkers", + "value" + ], + "type": "object" + }, + "AnnotationLayerRestApi.get": { + "properties": { + "descr": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "AnnotationLayerRestApi.get_list": { + "properties": { + "changed_by": { + "$ref": "#/components/schemas/AnnotationLayerRestApi.get_list.User1" + }, + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/AnnotationLayerRestApi.get_list.User" + }, + "created_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "descr": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "AnnotationLayerRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "AnnotationLayerRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "AnnotationLayerRestApi.post": { + "properties": { + "descr": { + "description": "Give a description for this annotation layer", + "nullable": true, + "type": "string" + }, + "name": { + "description": "The annotation layer name", + "maxLength": 250, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "AnnotationLayerRestApi.put": { + "properties": { + "descr": { + "description": "Give a description for this annotation layer", + "type": "string" + }, + "name": { + "description": "The annotation layer name", + "maxLength": 250, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "AnnotationRestApi.get": { + "properties": { + "end_dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "json_metadata": { + "nullable": true, + "type": "string" + }, + "layer": { + "$ref": "#/components/schemas/AnnotationRestApi.get.AnnotationLayer" + }, + "long_descr": { + "nullable": true, + "type": "string" + }, + "short_descr": { + "maxLength": 500, + "nullable": true, + "type": "string" + }, + "start_dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + } + }, + "required": [ + "layer" + ], + "type": "object" + }, + "AnnotationRestApi.get.AnnotationLayer": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "AnnotationRestApi.get_list": { + "properties": { + "changed_by": { + "$ref": "#/components/schemas/AnnotationRestApi.get_list.User" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/AnnotationRestApi.get_list.User1" + }, + "end_dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "long_descr": { + "nullable": true, + "type": "string" + }, + "short_descr": { + "maxLength": 500, + "nullable": true, + "type": "string" + }, + "start_dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "AnnotationRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "first_name" + ], + "type": "object" + }, + "AnnotationRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "first_name" + ], + "type": "object" + }, + "AnnotationRestApi.post": { + "properties": { + "end_dttm": { + "description": "The annotation end date time", + "format": "date-time", + "type": "string" + }, + "json_metadata": { + "description": "JSON metadata", + "nullable": true, + "type": "string" + }, + "long_descr": { + "description": "A long description", + "nullable": true, + "type": "string" + }, + "short_descr": { + "description": "A short description", + "maxLength": 500, + "minLength": 1, + "type": "string" + }, + "start_dttm": { + "description": "The annotation start date time", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "end_dttm", + "short_descr", + "start_dttm" + ], + "type": "object" + }, + "AnnotationRestApi.put": { + "properties": { + "end_dttm": { + "description": "The annotation end date time", + "format": "date-time", + "type": "string" + }, + "json_metadata": { + "description": "JSON metadata", + "nullable": true, + "type": "string" + }, + "long_descr": { + "description": "A long description", + "nullable": true, + "type": "string" + }, + "short_descr": { + "description": "A short description", + "maxLength": 500, + "minLength": 1, + "type": "string" + }, + "start_dttm": { + "description": "The annotation start date time", + "format": "date-time", + "type": "string" + } + }, + "type": "object" + }, + "AvailableDomainsSchema": { + "properties": { + "domains": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "CacheInvalidationRequestSchema": { + "properties": { + "datasource_uids": { + "description": "The uid of the dataset/datasource this new chart will use. A complete datasource identification needs `datasource_uid` ", + "items": { + "type": "string" + }, + "type": "array" + }, + "datasources": { + "description": "A list of the data source and database names", + "items": { + "$ref": "#/components/schemas/Datasource" + }, + "type": "array" + } + }, + "type": "object" + }, + "CacheRestApi.get": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "CacheRestApi.get_list": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "CacheRestApi.post": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "CacheRestApi.put": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "CatalogsResponseSchema": { + "properties": { + "result": { + "items": { + "description": "A database catalog name", + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "ChartCacheScreenshotResponseSchema": { + "properties": { + "cache_key": { + "description": "The cache key", + "type": "string" + }, + "chart_url": { + "description": "The url to render the chart", + "type": "string" + }, + "image_url": { + "description": "The url to fetch the screenshot", + "type": "string" + }, + "task_status": { + "description": "The status of the async screenshot", + "type": "string" + }, + "task_updated_at": { + "description": "The timestamp of the last change in status", + "type": "string" + } + }, + "type": "object" + }, + "ChartCacheWarmUpRequestSchema": { + "properties": { + "chart_id": { + "description": "The ID of the chart to warm up cache for", + "type": "integer" + }, + "dashboard_id": { + "description": "The ID of the dashboard to get filters for when warming cache", + "type": "integer" + }, + "extra_filters": { + "description": "Extra filters to apply when warming up cache", + "type": "string" + } + }, + "required": [ + "chart_id" + ], + "type": "object" + }, + "ChartCacheWarmUpResponseSchema": { + "properties": { + "result": { + "description": "A list of each chart's warmup status and errors if any", + "items": { + "$ref": "#/components/schemas/ChartCacheWarmUpResponseSingle" + }, + "type": "array" + } + }, + "type": "object" + }, + "ChartCacheWarmUpResponseSingle": { + "properties": { + "chart_id": { + "description": "The ID of the chart the status belongs to", + "type": "integer" + }, + "viz_error": { + "description": "Error that occurred when warming cache for chart", + "type": "string" + }, + "viz_status": { + "description": "Status of the underlying query for the viz", + "type": "string" + } + }, + "type": "object" + }, + "ChartDataAdhocMetricSchema": { + "properties": { + "aggregate": { + "description": "Aggregation operator.Only required for simple expression types.", + "enum": [ + "AVG", + "COUNT", + "COUNT_DISTINCT", + "MAX", + "MIN", + "SUM" + ], + "type": "string" + }, + "column": { + "$ref": "#/components/schemas/ChartDataColumn" + }, + "expressionType": { + "description": "Simple or SQL metric", + "enum": [ + "SIMPLE", + "SQL" + ], + "example": "SQL", + "type": "string" + }, + "hasCustomLabel": { + "description": "When false, the label will be automatically generated based on the aggregate expression. When true, a custom label has to be specified.", + "example": true, + "type": "boolean" + }, + "isExtra": { + "description": "Indicates if the filter has been added by a filter component as opposed to being a part of the original query.", + "type": "boolean" + }, + "label": { + "description": "Label for the metric. Is automatically generated unlesshasCustomLabel is true, in which case label must be defined.", + "example": "Weighted observations", + "type": "string" + }, + "optionName": { + "description": "Unique identifier. Can be any string value, as long as all metrics have a unique identifier. If undefined, a random namewill be generated.", + "example": "metric_aec60732-fac0-4b17-b736-93f1a5c93e30", + "type": "string" + }, + "sqlExpression": { + "description": "The metric as defined by a SQL aggregate expression. Only required for SQL expression type.", + "example": "SUM(weight * observations) / SUM(weight)", + "type": "string" + }, + "timeGrain": { + "description": "Optional time grain for temporal filters", + "example": "PT1M", + "type": "string" + } + }, + "required": [ + "expressionType" + ], + "type": "object" + }, + "ChartDataAggregateOptionsSchema": { + "properties": { + "aggregates": { + "description": "The keys are the name of the aggregate column to be created, and the values specify the details of how to apply the aggregation. If an operator requires additional options, these can be passed here to be unpacked in the operator call. The following numpy operators are supported: average, argmin, argmax, cumsum, cumprod, max, mean, median, nansum, nanmin, nanmax, nanmean, nanmedian, min, percentile, prod, product, std, sum, var. Any options required by the operator can be passed to the `options` object.\n\nIn the example, a new column `first_quantile` is created based on values in the column `my_col` using the `percentile` operator with the `q=0.25` parameter.", + "example": { + "first_quantile": { + "column": "my_col", + "operator": "percentile", + "options": { + "q": 0.25 + } + } + }, + "type": "object" + } + }, + "type": "object" + }, + "ChartDataAsyncResponseSchema": { + "properties": { + "channel_id": { + "description": "Unique session async channel ID", + "type": "string" + }, + "job_id": { + "description": "Unique async job ID", + "type": "string" + }, + "result_url": { + "description": "Unique result URL for fetching async query data", + "type": "string" + }, + "status": { + "description": "Status value for async job", + "type": "string" + }, + "user_id": { + "description": "Requesting user ID", + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "ChartDataBoxplotOptionsSchema": { + "properties": { + "groupby": { + "items": { + "description": "Columns by which to group the query.", + "type": "string" + }, + "nullable": true, + "type": "array" + }, + "metrics": { + "description": "Aggregate expressions. Metrics can be passed as both references to datasource metrics (strings), or ad-hoc metricswhich are defined only within the query object. See `ChartDataAdhocMetricSchema` for the structure of ad-hoc metrics. When metrics is undefined or null, the query is executed without a groupby. However, when metrics is an array (length >= 0), a groupby clause is added to the query.", + "items": {}, + "nullable": true, + "type": "array" + }, + "percentiles": { + "description": "Upper and lower percentiles for percentile whisker type.", + "example": [ + 1, + 99 + ] + }, + "whisker_type": { + "description": "Whisker type. Any numpy function will work.", + "enum": [ + "tukey", + "min/max", + "percentile" + ], + "example": "tukey", + "type": "string" + } + }, + "required": [ + "whisker_type" + ], + "type": "object" + }, + "ChartDataColumn": { + "properties": { + "column_name": { + "description": "The name of the target column", + "example": "mycol", + "type": "string" + }, + "type": { + "description": "Type of target column", + "example": "BIGINT", + "type": "string" + } + }, + "type": "object" + }, + "ChartDataContributionOptionsSchema": { + "properties": { + "orientation": { + "description": "Should cell values be calculated across the row or column.", + "enum": [ + "row", + "column" + ], + "example": "row", + "type": "string" + } + }, + "required": [ + "orientation" + ], + "type": "object" + }, + "ChartDataDatasource": { + "properties": { + "id": { + "description": "Datasource id", + "type": "integer" + }, + "type": { + "description": "Datasource type", + "enum": [ + "table", + "dataset", + "query", + "saved_query", + "view" + ], + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "ChartDataExtras": { + "properties": { + "having": { + "description": "HAVING clause to be added to aggregate queries using AND operator.", + "type": "string" + }, + "instant_time_comparison_range": { + "description": "This is only set using the new time comparison controls that is made available in some plugins behind the experimental feature flag.", + "nullable": true, + "type": "string" + }, + "relative_end": { + "description": "End time for relative time deltas. Default: `config[\"DEFAULT_RELATIVE_START_TIME\"]`", + "enum": [ + "today", + "now" + ], + "type": "string" + }, + "relative_start": { + "description": "Start time for relative time deltas. Default: `config[\"DEFAULT_RELATIVE_START_TIME\"]`", + "enum": [ + "today", + "now" + ], + "type": "string" + }, + "time_grain_sqla": { + "description": "To what level of granularity should the temporal column be aggregated. Supports [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) durations.", + "enum": [ + "PT1S", + "PT5S", + "PT30S", + "PT1M", + "PT5M", + "PT10M", + "PT15M", + "PT30M", + "PT1H", + "PT6H", + "P1D", + "P1W", + "P1M", + "P3M", + "P1Y", + "1969-12-28T00:00:00Z/P1W", + "1969-12-29T00:00:00Z/P1W", + "P1W/1970-01-03T00:00:00Z", + "P1W/1970-01-04T00:00:00Z" + ], + "example": "P1D", + "nullable": true, + "type": "string" + }, + "where": { + "description": "WHERE clause to be added to queries using AND operator.", + "type": "string" + } + }, + "type": "object" + }, + "ChartDataFilter": { + "properties": { + "col": { + "description": "The column to filter by. Can be either a string (physical or saved expression) or an object (adhoc column)", + "example": "country" + }, + "grain": { + "description": "Optional time grain for temporal filters", + "example": "PT1M", + "type": "string" + }, + "isExtra": { + "description": "Indicates if the filter has been added by a filter component as opposed to being a part of the original query.", + "type": "boolean" + }, + "op": { + "description": "The comparison operator.", + "enum": [ + "==", + "!=", + ">", + "<", + ">=", + "<=", + "LIKE", + "NOT LIKE", + "ILIKE", + "IS NULL", + "IS NOT NULL", + "IN", + "NOT IN", + "IS TRUE", + "IS FALSE", + "TEMPORAL_RANGE" + ], + "example": "IN", + "type": "string" + }, + "val": { + "description": "The value or values to compare against. Can be a string, integer, decimal, None or list, depending on the operator.", + "example": [ + "China", + "France", + "Japan" + ], + "nullable": true + } + }, + "required": [ + "col", + "op" + ], + "type": "object" + }, + "ChartDataGeodeticParseOptionsSchema": { + "properties": { + "altitude": { + "description": "Name of target column for decoded altitude. If omitted, altitude information in geodetic string is ignored.", + "type": "string" + }, + "geodetic": { + "description": "Name of source column containing geodetic point strings", + "type": "string" + }, + "latitude": { + "description": "Name of target column for decoded latitude", + "type": "string" + }, + "longitude": { + "description": "Name of target column for decoded longitude", + "type": "string" + } + }, + "required": [ + "geodetic", + "latitude", + "longitude" + ], + "type": "object" + }, + "ChartDataGeohashDecodeOptionsSchema": { + "properties": { + "geohash": { + "description": "Name of source column containing geohash string", + "type": "string" + }, + "latitude": { + "description": "Name of target column for decoded latitude", + "type": "string" + }, + "longitude": { + "description": "Name of target column for decoded longitude", + "type": "string" + } + }, + "required": [ + "geohash", + "latitude", + "longitude" + ], + "type": "object" + }, + "ChartDataGeohashEncodeOptionsSchema": { + "properties": { + "geohash": { + "description": "Name of target column for encoded geohash string", + "type": "string" + }, + "latitude": { + "description": "Name of source latitude column", + "type": "string" + }, + "longitude": { + "description": "Name of source longitude column", + "type": "string" + } + }, + "required": [ + "geohash", + "latitude", + "longitude" + ], + "type": "object" + }, + "ChartDataPivotOptionsSchema": { + "properties": { + "aggregates": { + "description": "The keys are the name of the aggregate column to be created, and the values specify the details of how to apply the aggregation. If an operator requires additional options, these can be passed here to be unpacked in the operator call. The following numpy operators are supported: average, argmin, argmax, cumsum, cumprod, max, mean, median, nansum, nanmin, nanmax, nanmean, nanmedian, min, percentile, prod, product, std, sum, var. Any options required by the operator can be passed to the `options` object.\n\nIn the example, a new column `first_quantile` is created based on values in the column `my_col` using the `percentile` operator with the `q=0.25` parameter.", + "example": { + "first_quantile": { + "column": "my_col", + "operator": "percentile", + "options": { + "q": 0.25 + } + } + }, + "type": "object" + }, + "column_fill_value": { + "description": "Value to replace missing pivot columns names with.", + "type": "string" + }, + "columns": { + "description": "Columns to group by on the table columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "drop_missing_columns": { + "description": "Do not include columns whose entries are all missing (default: `true`).", + "type": "boolean" + }, + "marginal_distribution_name": { + "description": "Name of marginal distribution row/column. (default: `All`)", + "type": "string" + }, + "marginal_distributions": { + "description": "Add totals for row/column. (default: `false`)", + "type": "boolean" + }, + "metric_fill_value": { + "description": "Value to replace missing values with in aggregate calculations.", + "type": "number" + } + }, + "type": "object" + }, + "ChartDataPostProcessingOperation": { + "properties": { + "operation": { + "description": "Post processing operation type", + "enum": [ + "aggregate", + "boxplot", + "compare", + "contribution", + "cum", + "diff", + "escape_separator", + "flatten", + "geodetic_parse", + "geohash_decode", + "geohash_encode", + "histogram", + "pivot", + "prophet", + "rank", + "rename", + "resample", + "rolling", + "select", + "sort", + "unescape_separator" + ], + "example": "aggregate", + "type": "string" + }, + "options": { + "description": "Options specifying how to perform the operation. Please refer to the respective post processing operation option schemas. For example, `ChartDataPostProcessingOperationOptions` specifies the required options for the pivot operation.", + "example": { + "aggregates": { + "age_mean": { + "column": "age", + "operator": "mean" + }, + "age_q1": { + "column": "age", + "operator": "percentile", + "options": { + "q": 0.25 + } + } + }, + "groupby": [ + "country", + "gender" + ] + }, + "type": "object" + } + }, + "required": [ + "operation" + ], + "type": "object" + }, + "ChartDataProphetOptionsSchema": { + "properties": { + "confidence_interval": { + "description": "Width of predicted confidence interval", + "example": 0.8, + "maximum": 1.0, + "minimum": 0.0, + "type": "number" + }, + "monthly_seasonality": { + "description": "Should monthly seasonality be applied. An integer value will specify Fourier order of seasonality, `None` will automatically detect seasonality.", + "example": false + }, + "periods": { + "description": "Time periods (in units of `time_grain`) to predict into the future", + "example": 7, + "type": "integer" + }, + "time_grain": { + "description": "Time grain used to specify time period increments in prediction. Supports [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) durations.", + "enum": [ + "PT1S", + "PT5S", + "PT30S", + "PT1M", + "PT5M", + "PT10M", + "PT15M", + "PT30M", + "PT1H", + "PT6H", + "P1D", + "P1W", + "P1M", + "P3M", + "P1Y", + "1969-12-28T00:00:00Z/P1W", + "1969-12-29T00:00:00Z/P1W", + "P1W/1970-01-03T00:00:00Z", + "P1W/1970-01-04T00:00:00Z" + ], + "example": "P1D", + "type": "string" + }, + "weekly_seasonality": { + "description": "Should weekly seasonality be applied. An integer value will specify Fourier order of seasonality, `None` will automatically detect seasonality.", + "example": false + }, + "yearly_seasonality": { + "description": "Should yearly seasonality be applied. An integer value will specify Fourier order of seasonality, `None` will automatically detect seasonality.", + "example": false + } + }, + "required": [ + "confidence_interval", + "periods", + "time_grain" + ], + "type": "object" + }, + "ChartDataQueryContextSchema": { + "properties": { + "custom_cache_timeout": { + "description": "Override the default cache timeout", + "nullable": true, + "type": "integer" + }, + "datasource": { + "$ref": "#/components/schemas/ChartDataDatasource" + }, + "force": { + "description": "Should the queries be forced to load from the source. Default: `false`", + "nullable": true, + "type": "boolean" + }, + "form_data": { + "nullable": true + }, + "queries": { + "items": { + "$ref": "#/components/schemas/ChartDataQueryObject" + }, + "type": "array" + }, + "result_format": { + "enum": [ + "csv", + "json", + "xlsx" + ] + }, + "result_type": { + "enum": [ + "columns", + "full", + "query", + "results", + "samples", + "timegrains", + "post_processed", + "drill_detail" + ] + } + }, + "type": "object" + }, + "ChartDataQueryObject": { + "properties": { + "annotation_layers": { + "description": "Annotation layers to apply to chart", + "items": { + "$ref": "#/components/schemas/AnnotationLayer" + }, + "nullable": true, + "type": "array" + }, + "applied_time_extras": { + "description": "A mapping of temporal extras that have been applied to the query", + "example": { + "__time_range": "1 year ago : now" + }, + "nullable": true, + "type": "object" + }, + "apply_fetch_values_predicate": { + "description": "Add fetch values predicate (where clause) to query if defined in datasource", + "nullable": true, + "type": "boolean" + }, + "columns": { + "description": "Columns which to select in the query.", + "items": {}, + "nullable": true, + "type": "array" + }, + "datasource": { + "allOf": [ + { + "$ref": "#/components/schemas/ChartDataDatasource" + } + ], + "nullable": true + }, + "extras": { + "allOf": [ + { + "$ref": "#/components/schemas/ChartDataExtras" + } + ], + "description": "Extra parameters to add to the query.", + "nullable": true + }, + "filters": { + "items": { + "$ref": "#/components/schemas/ChartDataFilter" + }, + "nullable": true, + "type": "array" + }, + "granularity": { + "description": "Name of temporal column used for time filtering. ", + "nullable": true, + "type": "string" + }, + "granularity_sqla": { + "deprecated": true, + "description": "Name of temporal column used for time filtering for SQL datasources. This field is deprecated, use `granularity` instead.", + "nullable": true, + "type": "string" + }, + "groupby": { + "description": "Columns by which to group the query. This field is deprecated, use `columns` instead.", + "items": {}, + "nullable": true, + "type": "array" + }, + "having": { + "deprecated": true, + "description": "HAVING clause to be added to aggregate queries using AND operator. This field is deprecated and should be passed to `extras`.", + "nullable": true, + "type": "string" + }, + "is_rowcount": { + "description": "Should the rowcount of the actual query be returned", + "nullable": true, + "type": "boolean" + }, + "is_timeseries": { + "description": "Is the `query_object` a timeseries.", + "nullable": true, + "type": "boolean" + }, + "metrics": { + "description": "Aggregate expressions. Metrics can be passed as both references to datasource metrics (strings), or ad-hoc metricswhich are defined only within the query object. See `ChartDataAdhocMetricSchema` for the structure of ad-hoc metrics.", + "items": {}, + "nullable": true, + "type": "array" + }, + "order_desc": { + "description": "Reverse order. Default: `false`", + "nullable": true, + "type": "boolean" + }, + "orderby": { + "description": "Expects a list of lists where the first element is the column name which to sort by, and the second element is a boolean.", + "example": [ + [ + "my_col_1", + false + ], + [ + "my_col_2", + true + ] + ], + "items": {}, + "nullable": true, + "type": "array" + }, + "post_processing": { + "description": "Post processing operations to be applied to the result set. Operations are applied to the result set in sequential order.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/ChartDataPostProcessingOperation" + } + ], + "nullable": true + }, + "nullable": true, + "type": "array" + }, + "result_type": { + "enum": [ + "columns", + "full", + "query", + "results", + "samples", + "timegrains", + "post_processed", + "drill_detail" + ], + "nullable": true + }, + "row_limit": { + "description": "Maximum row count (0=disabled). Default: `config[\"ROW_LIMIT\"]`", + "minimum": 0, + "nullable": true, + "type": "integer" + }, + "row_offset": { + "description": "Number of rows to skip. Default: `0`", + "minimum": 0, + "nullable": true, + "type": "integer" + }, + "series_columns": { + "description": "Columns to use when limiting series count. All columns must be present in the `columns` property. Requires `series_limit` and `series_limit_metric` to be set.", + "items": {}, + "nullable": true, + "type": "array" + }, + "series_limit": { + "description": "Maximum number of series. Requires `series` and `series_limit_metric` to be set.", + "nullable": true, + "type": "integer" + }, + "series_limit_metric": { + "description": "Metric used to limit timeseries queries by. Requires `series` and `series_limit` to be set.", + "nullable": true + }, + "time_offsets": { + "items": { + "type": "string" + }, + "nullable": true, + "type": "array" + }, + "time_range": { + "description": "A time rage, either expressed as a colon separated string `since : until` or human readable freeform. Valid formats for `since` and `until` are: \n- ISO 8601\n- X days/years/hours/day/year/weeks\n- X days/years/hours/day/year/weeks ago\n- X days/years/hours/day/year/weeks from now\n\nAdditionally, the following freeform can be used:\n\n- Last day\n- Last week\n- Last month\n- Last quarter\n- Last year\n- No filter\n- Last X seconds/minutes/hours/days/weeks/months/years\n- Next X seconds/minutes/hours/days/weeks/months/years\n", + "example": "Last week", + "nullable": true, + "type": "string" + }, + "time_shift": { + "description": "A human-readable date/time string. Please refer to [parsdatetime](https://github.com/bear/parsedatetime) documentation for details on valid values.", + "nullable": true, + "type": "string" + }, + "timeseries_limit": { + "description": "Maximum row count for timeseries queries. This field is deprecated, use `series_limit` instead.Default: `0`", + "nullable": true, + "type": "integer" + }, + "timeseries_limit_metric": { + "description": "Metric used to limit timeseries queries by. This field is deprecated, use `series_limit_metric` instead.", + "nullable": true + }, + "url_params": { + "additionalProperties": { + "description": "The value of the query parameter", + "type": "string" + }, + "description": "Optional query parameters passed to a dashboard or Explore view", + "nullable": true, + "type": "object" + }, + "where": { + "deprecated": true, + "description": "WHERE clause to be added to queries using AND operator.This field is deprecated and should be passed to `extras`.", + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "ChartDataResponseResult": { + "properties": { + "annotation_data": { + "description": "All requested annotation data", + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "nullable": true, + "type": "array" + }, + "applied_filters": { + "description": "A list with applied filters", + "items": { + "type": "object" + }, + "type": "array" + }, + "cache_key": { + "description": "Unique cache key for query object", + "nullable": true, + "type": "string" + }, + "cache_timeout": { + "description": "Cache timeout in following order: custom timeout, datasource timeout, cache default timeout, config default cache timeout.", + "nullable": true, + "type": "integer" + }, + "cached_dttm": { + "description": "Cache timestamp", + "nullable": true, + "type": "string" + }, + "colnames": { + "description": "A list of column names", + "items": { + "type": "string" + }, + "type": "array" + }, + "coltypes": { + "description": "A list of generic data types of each column", + "items": { + "type": "integer" + }, + "type": "array" + }, + "data": { + "description": "A list with results", + "items": { + "type": "object" + }, + "type": "array" + }, + "error": { + "description": "Error", + "nullable": true, + "type": "string" + }, + "from_dttm": { + "description": "Start timestamp of time range", + "nullable": true, + "type": "integer" + }, + "is_cached": { + "description": "Is the result cached", + "type": "boolean" + }, + "query": { + "description": "The executed query statement", + "type": "string" + }, + "rejected_filters": { + "description": "A list with rejected filters", + "items": { + "type": "object" + }, + "type": "array" + }, + "rowcount": { + "description": "Amount of rows in result set", + "type": "integer" + }, + "stacktrace": { + "description": "Stacktrace if there was an error", + "nullable": true, + "type": "string" + }, + "status": { + "description": "Status of the query", + "enum": [ + "stopped", + "failed", + "pending", + "running", + "scheduled", + "success", + "timed_out" + ], + "type": "string" + }, + "to_dttm": { + "description": "End timestamp of time range", + "nullable": true, + "type": "integer" + } + }, + "required": [ + "cache_key", + "cache_timeout", + "cached_dttm", + "is_cached", + "query" + ], + "type": "object" + }, + "ChartDataResponseSchema": { + "properties": { + "result": { + "description": "A list of results for each corresponding query in the request.", + "items": { + "$ref": "#/components/schemas/ChartDataResponseResult" + }, + "type": "array" + } + }, + "type": "object" + }, + "ChartDataRestApi.get": { + "properties": { + "cache_timeout": { + "nullable": true, + "type": "integer" + }, + "certification_details": { + "nullable": true, + "type": "string" + }, + "certified_by": { + "nullable": true, + "type": "string" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "dashboards": { + "$ref": "#/components/schemas/ChartDataRestApi.get.Dashboard" + }, + "description": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_managed_externally": { + "type": "boolean" + }, + "owners": { + "$ref": "#/components/schemas/ChartDataRestApi.get.User" + }, + "params": { + "nullable": true, + "type": "string" + }, + "query_context": { + "nullable": true, + "type": "string" + }, + "slice_name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "tags": { + "$ref": "#/components/schemas/ChartDataRestApi.get.Tag" + }, + "thumbnail_url": { + "readOnly": true + }, + "url": { + "readOnly": true + }, + "viz_type": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "ChartDataRestApi.get.Dashboard": { + "properties": { + "dashboard_title": { + "maxLength": 500, + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "json_metadata": { + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "ChartDataRestApi.get.Tag": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "type": { + "enum": [ + 1, + 2, + 3, + 4 + ] + } + }, + "type": "object" + }, + "ChartDataRestApi.get.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ChartDataRestApi.get_list": { + "properties": { + "cache_timeout": { + "nullable": true, + "type": "integer" + }, + "certification_details": { + "nullable": true, + "type": "string" + }, + "certified_by": { + "nullable": true, + "type": "string" + }, + "changed_by": { + "$ref": "#/components/schemas/ChartDataRestApi.get_list.User" + }, + "changed_by_name": { + "readOnly": true + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "changed_on_dttm": { + "readOnly": true + }, + "changed_on_utc": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/ChartDataRestApi.get_list.User1" + }, + "created_by_name": { + "readOnly": true + }, + "created_on_delta_humanized": { + "readOnly": true + }, + "dashboards": { + "$ref": "#/components/schemas/ChartDataRestApi.get_list.Dashboard" + }, + "datasource_id": { + "nullable": true, + "type": "integer" + }, + "datasource_name_text": { + "readOnly": true + }, + "datasource_type": { + "maxLength": 200, + "nullable": true, + "type": "string" + }, + "datasource_url": { + "readOnly": true + }, + "description": { + "nullable": true, + "type": "string" + }, + "description_markeddown": { + "readOnly": true + }, + "edit_url": { + "readOnly": true + }, + "form_data": { + "readOnly": true + }, + "id": { + "type": "integer" + }, + "is_managed_externally": { + "type": "boolean" + }, + "last_saved_at": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "last_saved_by": { + "$ref": "#/components/schemas/ChartDataRestApi.get_list.User2" + }, + "owners": { + "$ref": "#/components/schemas/ChartDataRestApi.get_list.User3" + }, + "params": { + "nullable": true, + "type": "string" + }, + "slice_name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "slice_url": { + "readOnly": true + }, + "table": { + "$ref": "#/components/schemas/ChartDataRestApi.get_list.SqlaTable" + }, + "tags": { + "$ref": "#/components/schemas/ChartDataRestApi.get_list.Tag" + }, + "thumbnail_url": { + "readOnly": true + }, + "url": { + "readOnly": true + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + }, + "viz_type": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "ChartDataRestApi.get_list.Dashboard": { + "properties": { + "dashboard_title": { + "maxLength": 500, + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "ChartDataRestApi.get_list.SqlaTable": { + "properties": { + "default_endpoint": { + "nullable": true, + "type": "string" + }, + "table_name": { + "maxLength": 250, + "type": "string" + } + }, + "required": [ + "table_name" + ], + "type": "object" + }, + "ChartDataRestApi.get_list.Tag": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "type": { + "enum": [ + 1, + 2, + 3, + 4 + ] + } + }, + "type": "object" + }, + "ChartDataRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ChartDataRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ChartDataRestApi.get_list.User2": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ChartDataRestApi.get_list.User3": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ChartDataRestApi.post": { + "properties": { + "cache_timeout": { + "description": "Duration (in seconds) of the caching timeout for this chart. Note this defaults to the datasource/table timeout if undefined.", + "nullable": true, + "type": "integer" + }, + "certification_details": { + "description": "Details of the certification", + "nullable": true, + "type": "string" + }, + "certified_by": { + "description": "Person or group that has certified this chart", + "nullable": true, + "type": "string" + }, + "dashboards": { + "items": { + "description": "A list of dashboards to include this new chart to.", + "type": "integer" + }, + "type": "array" + }, + "datasource_id": { + "description": "The id of the dataset/datasource this new chart will use. A complete datasource identification needs `datasource_id` and `datasource_type`.", + "type": "integer" + }, + "datasource_name": { + "description": "The datasource name.", + "nullable": true, + "type": "string" + }, + "datasource_type": { + "description": "The type of dataset/datasource identified on `datasource_id`.", + "enum": [ + "table", + "dataset", + "query", + "saved_query", + "view" + ], + "type": "string" + }, + "description": { + "description": "A description of the chart propose.", + "nullable": true, + "type": "string" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "owners": { + "items": { + "description": "Owner are users ids allowed to delete or change this chart. If left empty you will be one of the owners of the chart.", + "type": "integer" + }, + "type": "array" + }, + "params": { + "description": "Parameters are generated dynamically when clicking the save or overwrite button in the explore view. This JSON object for power users who may want to alter specific parameters.", + "nullable": true, + "type": "string" + }, + "query_context": { + "description": "The query context represents the queries that need to run in order to generate the data the visualization, and in what format the data should be returned.", + "nullable": true, + "type": "string" + }, + "query_context_generation": { + "description": "The query context generation represents whether the query_contextis user generated or not so that it does not update user modifiedstate.", + "nullable": true, + "type": "boolean" + }, + "slice_name": { + "description": "The name of the chart.", + "maxLength": 250, + "minLength": 1, + "type": "string" + }, + "viz_type": { + "description": "The type of chart visualization used.", + "example": [ + "bar", + "area", + "table" + ], + "maxLength": 250, + "minLength": 0, + "type": "string" + } + }, + "required": [ + "datasource_id", + "datasource_type", + "slice_name" + ], + "type": "object" + }, + "ChartDataRestApi.put": { + "properties": { + "cache_timeout": { + "description": "Duration (in seconds) of the caching timeout for this chart. Note this defaults to the datasource/table timeout if undefined.", + "nullable": true, + "type": "integer" + }, + "certification_details": { + "description": "Details of the certification", + "nullable": true, + "type": "string" + }, + "certified_by": { + "description": "Person or group that has certified this chart", + "nullable": true, + "type": "string" + }, + "dashboards": { + "items": { + "description": "A list of dashboards to include this new chart to.", + "type": "integer" + }, + "type": "array" + }, + "datasource_id": { + "description": "The id of the dataset/datasource this new chart will use. A complete datasource identification needs `datasource_id` and `datasource_type`.", + "nullable": true, + "type": "integer" + }, + "datasource_type": { + "description": "The type of dataset/datasource identified on `datasource_id`.", + "enum": [ + "table", + "dataset", + "query", + "saved_query", + "view" + ], + "nullable": true, + "type": "string" + }, + "description": { + "description": "A description of the chart propose.", + "nullable": true, + "type": "string" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "owners": { + "items": { + "description": "Owner are users ids allowed to delete or change this chart. If left empty you will be one of the owners of the chart.", + "type": "integer" + }, + "type": "array" + }, + "params": { + "description": "Parameters are generated dynamically when clicking the save or overwrite button in the explore view. This JSON object for power users who may want to alter specific parameters.", + "nullable": true, + "type": "string" + }, + "query_context": { + "description": "The query context represents the queries that need to run in order to generate the data the visualization, and in what format the data should be returned.", + "nullable": true, + "type": "string" + }, + "query_context_generation": { + "description": "The query context generation represents whether the query_contextis user generated or not so that it does not update user modifiedstate.", + "nullable": true, + "type": "boolean" + }, + "slice_name": { + "description": "The name of the chart.", + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "tags": { + "items": { + "description": "Tags to be associated with the chart", + "type": "integer" + }, + "type": "array" + }, + "viz_type": { + "description": "The type of chart visualization used.", + "example": [ + "bar", + "area", + "table" + ], + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "ChartDataRollingOptionsSchema": { + "properties": { + "center": { + "description": "Should the label be at the center of the window.Default: `false`", + "example": false, + "type": "boolean" + }, + "min_periods": { + "description": "The minimum amount of periods required for a row to be included in the result set.", + "example": 7, + "type": "integer" + }, + "rolling_type": { + "description": "Type of rolling window. Any numpy function will work.", + "enum": [ + "average", + "argmin", + "argmax", + "cumsum", + "cumprod", + "max", + "mean", + "median", + "nansum", + "nanmin", + "nanmax", + "nanmean", + "nanmedian", + "nanpercentile", + "min", + "percentile", + "prod", + "product", + "std", + "sum", + "var" + ], + "example": "percentile", + "type": "string" + }, + "rolling_type_options": { + "description": "Optional options to pass to rolling method. Needed for e.g. quantile operation.", + "example": {}, + "type": "object" + }, + "win_type": { + "description": "Type of window function. See [SciPy window functions](https://docs.scipy.org/doc/scipy/reference /signal.windows.html#module-scipy.signal.windows) for more details. Some window functions require passing additional parameters to `rolling_type_options`. For instance, to use `gaussian`, the parameter `std` needs to be provided.", + "enum": [ + "boxcar", + "triang", + "blackman", + "hamming", + "bartlett", + "parzen", + "bohman", + "blackmanharris", + "nuttall", + "barthann", + "kaiser", + "gaussian", + "general_gaussian", + "slepian", + "exponential" + ], + "type": "string" + }, + "window": { + "description": "Size of the rolling window in days.", + "example": 7, + "type": "integer" + } + }, + "required": [ + "rolling_type", + "window" + ], + "type": "object" + }, + "ChartDataSelectOptionsSchema": { + "properties": { + "columns": { + "description": "Columns which to select from the input data, in the desired order. If columns are renamed, the original column name should be referenced here.", + "example": [ + "country", + "gender", + "age" + ], + "items": { + "type": "string" + }, + "type": "array" + }, + "exclude": { + "description": "Columns to exclude from selection.", + "example": [ + "my_temp_column" + ], + "items": { + "type": "string" + }, + "type": "array" + }, + "rename": { + "description": "columns which to rename, mapping source column to target column. For instance, `{'y': 'y2'}` will rename the column `y` to `y2`.", + "example": [ + { + "age": "average_age" + } + ], + "items": { + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "ChartDataSortOptionsSchema": { + "properties": { + "aggregates": { + "description": "The keys are the name of the aggregate column to be created, and the values specify the details of how to apply the aggregation. If an operator requires additional options, these can be passed here to be unpacked in the operator call. The following numpy operators are supported: average, argmin, argmax, cumsum, cumprod, max, mean, median, nansum, nanmin, nanmax, nanmean, nanmedian, min, percentile, prod, product, std, sum, var. Any options required by the operator can be passed to the `options` object.\n\nIn the example, a new column `first_quantile` is created based on values in the column `my_col` using the `percentile` operator with the `q=0.25` parameter.", + "example": { + "first_quantile": { + "column": "my_col", + "operator": "percentile", + "options": { + "q": 0.25 + } + } + }, + "type": "object" + }, + "columns": { + "description": "columns by by which to sort. The key specifies the column name, value specifies if sorting in ascending order.", + "example": { + "country": true, + "gender": false + }, + "type": "object" + } + }, + "required": [ + "columns" + ], + "type": "object" + }, + "ChartEntityResponseSchema": { + "properties": { + "cache_timeout": { + "description": "Duration (in seconds) of the caching timeout for this chart. Note this defaults to the datasource/table timeout if undefined.", + "type": "integer" + }, + "certification_details": { + "description": "Details of the certification", + "type": "string" + }, + "certified_by": { + "description": "Person or group that has certified this chart", + "type": "string" + }, + "changed_on": { + "description": "The ISO date that the chart was last changed.", + "format": "date-time", + "type": "string" + }, + "description": { + "description": "A description of the chart propose.", + "type": "string" + }, + "description_markeddown": { + "description": "Sanitized HTML version of the chart description.", + "type": "string" + }, + "form_data": { + "description": "Form data from the Explore controls used to form the chart's data query.", + "type": "object" + }, + "id": { + "description": "The id of the chart.", + "type": "integer" + }, + "slice_name": { + "description": "The name of the chart.", + "type": "string" + }, + "slice_url": { + "description": "The URL of the chart.", + "type": "string" + } + }, + "type": "object" + }, + "ChartFavStarResponseResult": { + "properties": { + "id": { + "description": "The Chart id", + "type": "integer" + }, + "value": { + "description": "The FaveStar value", + "type": "boolean" + } + }, + "type": "object" + }, + "ChartGetDatasourceObjectDataResponse": { + "properties": { + "datasource_id": { + "description": "The datasource identifier", + "type": "integer" + }, + "datasource_type": { + "description": "The datasource type", + "type": "integer" + } + }, + "type": "object" + }, + "ChartGetDatasourceObjectResponse": { + "properties": { + "label": { + "description": "The name of the datasource", + "type": "string" + }, + "value": { + "$ref": "#/components/schemas/ChartGetDatasourceObjectDataResponse" + } + }, + "type": "object" + }, + "ChartGetDatasourceResponseSchema": { + "properties": { + "count": { + "description": "The total number of datasources", + "type": "integer" + }, + "result": { + "$ref": "#/components/schemas/ChartGetDatasourceObjectResponse" + } + }, + "type": "object" + }, + "ChartRestApi.get": { + "properties": { + "cache_timeout": { + "nullable": true, + "type": "integer" + }, + "certification_details": { + "nullable": true, + "type": "string" + }, + "certified_by": { + "nullable": true, + "type": "string" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "dashboards": { + "$ref": "#/components/schemas/ChartRestApi.get.Dashboard" + }, + "description": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_managed_externally": { + "type": "boolean" + }, + "owners": { + "$ref": "#/components/schemas/ChartRestApi.get.User" + }, + "params": { + "nullable": true, + "type": "string" + }, + "query_context": { + "nullable": true, + "type": "string" + }, + "slice_name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "tags": { + "$ref": "#/components/schemas/ChartRestApi.get.Tag" + }, + "thumbnail_url": { + "readOnly": true + }, + "url": { + "readOnly": true + }, + "viz_type": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "ChartRestApi.get.Dashboard": { + "properties": { + "dashboard_title": { + "maxLength": 500, + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "json_metadata": { + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "ChartRestApi.get.Tag": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "type": { + "enum": [ + 1, + 2, + 3, + 4 + ] + } + }, + "type": "object" + }, + "ChartRestApi.get.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ChartRestApi.get_list": { + "properties": { + "cache_timeout": { + "nullable": true, + "type": "integer" + }, + "certification_details": { + "nullable": true, + "type": "string" + }, + "certified_by": { + "nullable": true, + "type": "string" + }, + "changed_by": { + "$ref": "#/components/schemas/ChartRestApi.get_list.User" + }, + "changed_by_name": { + "readOnly": true + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "changed_on_dttm": { + "readOnly": true + }, + "changed_on_utc": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/ChartRestApi.get_list.User1" + }, + "created_by_name": { + "readOnly": true + }, + "created_on_delta_humanized": { + "readOnly": true + }, + "dashboards": { + "$ref": "#/components/schemas/ChartRestApi.get_list.Dashboard" + }, + "datasource_id": { + "nullable": true, + "type": "integer" + }, + "datasource_name_text": { + "readOnly": true + }, + "datasource_type": { + "maxLength": 200, + "nullable": true, + "type": "string" + }, + "datasource_url": { + "readOnly": true + }, + "description": { + "nullable": true, + "type": "string" + }, + "description_markeddown": { + "readOnly": true + }, + "edit_url": { + "readOnly": true + }, + "form_data": { + "readOnly": true + }, + "id": { + "type": "integer" + }, + "is_managed_externally": { + "type": "boolean" + }, + "last_saved_at": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "last_saved_by": { + "$ref": "#/components/schemas/ChartRestApi.get_list.User2" + }, + "owners": { + "$ref": "#/components/schemas/ChartRestApi.get_list.User3" + }, + "params": { + "nullable": true, + "type": "string" + }, + "slice_name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "slice_url": { + "readOnly": true + }, + "table": { + "$ref": "#/components/schemas/ChartRestApi.get_list.SqlaTable" + }, + "tags": { + "$ref": "#/components/schemas/ChartRestApi.get_list.Tag" + }, + "thumbnail_url": { + "readOnly": true + }, + "url": { + "readOnly": true + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + }, + "viz_type": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "ChartRestApi.get_list.Dashboard": { + "properties": { + "dashboard_title": { + "maxLength": 500, + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "ChartRestApi.get_list.SqlaTable": { + "properties": { + "default_endpoint": { + "nullable": true, + "type": "string" + }, + "table_name": { + "maxLength": 250, + "type": "string" + } + }, + "required": [ + "table_name" + ], + "type": "object" + }, + "ChartRestApi.get_list.Tag": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "type": { + "enum": [ + 1, + 2, + 3, + 4 + ] + } + }, + "type": "object" + }, + "ChartRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ChartRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ChartRestApi.get_list.User2": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ChartRestApi.get_list.User3": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ChartRestApi.post": { + "properties": { + "cache_timeout": { + "description": "Duration (in seconds) of the caching timeout for this chart. Note this defaults to the datasource/table timeout if undefined.", + "nullable": true, + "type": "integer" + }, + "certification_details": { + "description": "Details of the certification", + "nullable": true, + "type": "string" + }, + "certified_by": { + "description": "Person or group that has certified this chart", + "nullable": true, + "type": "string" + }, + "dashboards": { + "items": { + "description": "A list of dashboards to include this new chart to.", + "type": "integer" + }, + "type": "array" + }, + "datasource_id": { + "description": "The id of the dataset/datasource this new chart will use. A complete datasource identification needs `datasource_id` and `datasource_type`.", + "type": "integer" + }, + "datasource_name": { + "description": "The datasource name.", + "nullable": true, + "type": "string" + }, + "datasource_type": { + "description": "The type of dataset/datasource identified on `datasource_id`.", + "enum": [ + "table", + "dataset", + "query", + "saved_query", + "view" + ], + "type": "string" + }, + "description": { + "description": "A description of the chart propose.", + "nullable": true, + "type": "string" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "owners": { + "items": { + "description": "Owner are users ids allowed to delete or change this chart. If left empty you will be one of the owners of the chart.", + "type": "integer" + }, + "type": "array" + }, + "params": { + "description": "Parameters are generated dynamically when clicking the save or overwrite button in the explore view. This JSON object for power users who may want to alter specific parameters.", + "nullable": true, + "type": "string" + }, + "query_context": { + "description": "The query context represents the queries that need to run in order to generate the data the visualization, and in what format the data should be returned.", + "nullable": true, + "type": "string" + }, + "query_context_generation": { + "description": "The query context generation represents whether the query_contextis user generated or not so that it does not update user modifiedstate.", + "nullable": true, + "type": "boolean" + }, + "slice_name": { + "description": "The name of the chart.", + "maxLength": 250, + "minLength": 1, + "type": "string" + }, + "viz_type": { + "description": "The type of chart visualization used.", + "example": [ + "bar", + "area", + "table" + ], + "maxLength": 250, + "minLength": 0, + "type": "string" + } + }, + "required": [ + "datasource_id", + "datasource_type", + "slice_name" + ], + "type": "object" + }, + "ChartRestApi.put": { + "properties": { + "cache_timeout": { + "description": "Duration (in seconds) of the caching timeout for this chart. Note this defaults to the datasource/table timeout if undefined.", + "nullable": true, + "type": "integer" + }, + "certification_details": { + "description": "Details of the certification", + "nullable": true, + "type": "string" + }, + "certified_by": { + "description": "Person or group that has certified this chart", + "nullable": true, + "type": "string" + }, + "dashboards": { + "items": { + "description": "A list of dashboards to include this new chart to.", + "type": "integer" + }, + "type": "array" + }, + "datasource_id": { + "description": "The id of the dataset/datasource this new chart will use. A complete datasource identification needs `datasource_id` and `datasource_type`.", + "nullable": true, + "type": "integer" + }, + "datasource_type": { + "description": "The type of dataset/datasource identified on `datasource_id`.", + "enum": [ + "table", + "dataset", + "query", + "saved_query", + "view" + ], + "nullable": true, + "type": "string" + }, + "description": { + "description": "A description of the chart propose.", + "nullable": true, + "type": "string" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "owners": { + "items": { + "description": "Owner are users ids allowed to delete or change this chart. If left empty you will be one of the owners of the chart.", + "type": "integer" + }, + "type": "array" + }, + "params": { + "description": "Parameters are generated dynamically when clicking the save or overwrite button in the explore view. This JSON object for power users who may want to alter specific parameters.", + "nullable": true, + "type": "string" + }, + "query_context": { + "description": "The query context represents the queries that need to run in order to generate the data the visualization, and in what format the data should be returned.", + "nullable": true, + "type": "string" + }, + "query_context_generation": { + "description": "The query context generation represents whether the query_contextis user generated or not so that it does not update user modifiedstate.", + "nullable": true, + "type": "boolean" + }, + "slice_name": { + "description": "The name of the chart.", + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "tags": { + "items": { + "description": "Tags to be associated with the chart", + "type": "integer" + }, + "type": "array" + }, + "viz_type": { + "description": "The type of chart visualization used.", + "example": [ + "bar", + "area", + "table" + ], + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "CssTemplateRestApi.get": { + "properties": { + "changed_by": { + "$ref": "#/components/schemas/CssTemplateRestApi.get.User" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/CssTemplateRestApi.get.User1" + }, + "css": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "template_name": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "CssTemplateRestApi.get.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "CssTemplateRestApi.get.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "CssTemplateRestApi.get_list": { + "properties": { + "changed_by": { + "$ref": "#/components/schemas/CssTemplateRestApi.get_list.User" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/CssTemplateRestApi.get_list.User1" + }, + "created_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "css": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "template_name": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "CssTemplateRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "CssTemplateRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "CssTemplateRestApi.post": { + "properties": { + "css": { + "nullable": true, + "type": "string" + }, + "template_name": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "CssTemplateRestApi.put": { + "properties": { + "css": { + "nullable": true, + "type": "string" + }, + "template_name": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "DashboardCacheScreenshotResponseSchema": { + "properties": { + "cache_key": { + "description": "The cache key", + "type": "string" + }, + "dashboard_url": { + "description": "The url to render the dashboard", + "type": "string" + }, + "image_url": { + "description": "The url to fetch the screenshot", + "type": "string" + }, + "task_status": { + "description": "The status of the async screenshot", + "type": "string" + }, + "task_updated_at": { + "description": "The timestamp of the last change in status", + "type": "string" + } + }, + "type": "object" + }, + "DashboardCopySchema": { + "properties": { + "css": { + "description": "Override CSS for the dashboard.", + "type": "string" + }, + "dashboard_title": { + "description": "A title for the dashboard.", + "maxLength": 500, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "duplicate_slices": { + "description": "Whether or not to also copy all charts on the dashboard", + "type": "boolean" + }, + "json_metadata": { + "description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.", + "type": "string" + } + }, + "required": [ + "json_metadata" + ], + "type": "object" + }, + "DashboardDatasetSchema": { + "properties": { + "always_filter_main_dttm": { + "type": "boolean" + }, + "cache_timeout": { + "type": "integer" + }, + "column_formats": { + "type": "object" + }, + "column_names": { + "items": { + "type": "string" + }, + "type": "array" + }, + "column_types": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "columns": { + "items": { + "type": "object" + }, + "type": "array" + }, + "database": { + "$ref": "#/components/schemas/Database" + }, + "datasource_name": { + "type": "string" + }, + "default_endpoint": { + "type": "string" + }, + "edit_url": { + "type": "string" + }, + "fetch_values_predicate": { + "type": "string" + }, + "filter_select": { + "type": "boolean" + }, + "filter_select_enabled": { + "type": "boolean" + }, + "granularity_sqla": { + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "array" + }, + "health_check_message": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_sqllab_view": { + "type": "boolean" + }, + "main_dttm_col": { + "type": "string" + }, + "metrics": { + "items": { + "type": "object" + }, + "type": "array" + }, + "name": { + "type": "string" + }, + "normalize_columns": { + "type": "boolean" + }, + "offset": { + "type": "integer" + }, + "order_by_choices": { + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "array" + }, + "owners": { + "items": { + "type": "object" + }, + "type": "array" + }, + "params": { + "type": "string" + }, + "perm": { + "type": "string" + }, + "schema": { + "type": "string" + }, + "select_star": { + "type": "string" + }, + "sql": { + "type": "string" + }, + "table_name": { + "type": "string" + }, + "template_params": { + "type": "string" + }, + "time_grain_sqla": { + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "verbose_map": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, + "DashboardGetResponseSchema": { + "properties": { + "certification_details": { + "description": "Details of the certification", + "type": "string" + }, + "certified_by": { + "description": "Person or group that has certified this dashboard", + "type": "string" + }, + "changed_by": { + "$ref": "#/components/schemas/User" + }, + "changed_by_name": { + "type": "string" + }, + "changed_on": { + "format": "date-time", + "type": "string" + }, + "changed_on_delta_humanized": { + "type": "string" + }, + "charts": { + "items": { + "description": "The names of the dashboard's charts. Names are used for legacy reasons.", + "type": "string" + }, + "type": "array" + }, + "created_by": { + "$ref": "#/components/schemas/User" + }, + "created_on_delta_humanized": { + "type": "string" + }, + "css": { + "description": "Override CSS for the dashboard.", + "type": "string" + }, + "dashboard_title": { + "description": "A title for the dashboard.", + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "json_metadata": { + "description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.", + "type": "string" + }, + "owners": { + "items": { + "$ref": "#/components/schemas/User" + }, + "type": "array" + }, + "position_json": { + "description": "This json object describes the positioning of the widgets in the dashboard. It is dynamically generated when adjusting the widgets size and positions by using drag & drop in the dashboard view", + "type": "string" + }, + "published": { + "type": "boolean" + }, + "roles": { + "items": { + "$ref": "#/components/schemas/Roles" + }, + "type": "array" + }, + "slug": { + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/Tag" + }, + "type": "array" + }, + "thumbnail_url": { + "nullable": true, + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "DashboardPermalinkStateSchema": { + "properties": { + "activeTabs": { + "description": "Current active dashboard tabs", + "items": { + "type": "string" + }, + "nullable": true, + "type": "array" + }, + "anchor": { + "description": "Optional anchor link added to url hash", + "nullable": true, + "type": "string" + }, + "dataMask": { + "description": "Data mask used for native filter state", + "nullable": true, + "type": "object" + }, + "urlParams": { + "description": "URL Parameters", + "items": { + "description": "URL Parameter key-value pair", + "nullable": true + }, + "nullable": true, + "type": "array" + } + }, + "type": "object" + }, + "DashboardRestApi.get": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "DashboardRestApi.get_list": { + "properties": { + "certification_details": { + "nullable": true, + "type": "string" + }, + "certified_by": { + "nullable": true, + "type": "string" + }, + "changed_by": { + "$ref": "#/components/schemas/DashboardRestApi.get_list.User" + }, + "changed_by_name": { + "readOnly": true + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "changed_on_utc": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/DashboardRestApi.get_list.User1" + }, + "created_on_delta_humanized": { + "readOnly": true + }, + "dashboard_title": { + "maxLength": 500, + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_managed_externally": { + "type": "boolean" + }, + "owners": { + "$ref": "#/components/schemas/DashboardRestApi.get_list.User2" + }, + "published": { + "nullable": true, + "type": "boolean" + }, + "roles": { + "$ref": "#/components/schemas/DashboardRestApi.get_list.Role" + }, + "slug": { + "maxLength": 255, + "nullable": true, + "type": "string" + }, + "status": { + "readOnly": true + }, + "tags": { + "$ref": "#/components/schemas/DashboardRestApi.get_list.Tag" + }, + "thumbnail_url": { + "readOnly": true + }, + "url": { + "readOnly": true + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "DashboardRestApi.get_list.Role": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "DashboardRestApi.get_list.Tag": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "type": { + "enum": [ + 1, + 2, + 3, + 4 + ] + } + }, + "type": "object" + }, + "DashboardRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DashboardRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DashboardRestApi.get_list.User2": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DashboardRestApi.post": { + "properties": { + "certification_details": { + "description": "Details of the certification", + "nullable": true, + "type": "string" + }, + "certified_by": { + "description": "Person or group that has certified this dashboard", + "nullable": true, + "type": "string" + }, + "css": { + "description": "Override CSS for the dashboard.", + "type": "string" + }, + "dashboard_title": { + "description": "A title for the dashboard.", + "maxLength": 500, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "json_metadata": { + "description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.", + "type": "string" + }, + "owners": { + "items": { + "description": "Owner are users ids allowed to delete or change this dashboard. If left empty you will be one of the owners of the dashboard.", + "type": "integer" + }, + "type": "array" + }, + "position_json": { + "description": "This json object describes the positioning of the widgets in the dashboard. It is dynamically generated when adjusting the widgets size and positions by using drag & drop in the dashboard view", + "type": "string" + }, + "published": { + "description": "Determines whether or not this dashboard is visible in the list of all dashboards.", + "type": "boolean" + }, + "roles": { + "items": { + "description": "Roles is a list which defines access to the dashboard. These roles are always applied in addition to restrictions on dataset level access. If no roles defined then the dashboard is available to all roles.", + "type": "integer" + }, + "type": "array" + }, + "slug": { + "description": "Unique identifying part for the web address of the dashboard.", + "maxLength": 255, + "minLength": 1, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "DashboardRestApi.put": { + "properties": { + "certification_details": { + "description": "Details of the certification", + "nullable": true, + "type": "string" + }, + "certified_by": { + "description": "Person or group that has certified this dashboard", + "nullable": true, + "type": "string" + }, + "css": { + "description": "Override CSS for the dashboard.", + "nullable": true, + "type": "string" + }, + "dashboard_title": { + "description": "A title for the dashboard.", + "maxLength": 500, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "json_metadata": { + "description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.", + "nullable": true, + "type": "string" + }, + "owners": { + "items": { + "description": "Owner are users ids allowed to delete or change this dashboard. If left empty you will be one of the owners of the dashboard.", + "nullable": true, + "type": "integer" + }, + "type": "array" + }, + "position_json": { + "description": "This json object describes the positioning of the widgets in the dashboard. It is dynamically generated when adjusting the widgets size and positions by using drag & drop in the dashboard view", + "nullable": true, + "type": "string" + }, + "published": { + "description": "Determines whether or not this dashboard is visible in the list of all dashboards.", + "nullable": true, + "type": "boolean" + }, + "roles": { + "items": { + "description": "Roles is a list which defines access to the dashboard. These roles are always applied in addition to restrictions on dataset level access. If no roles defined then the dashboard is available to all roles.", + "nullable": true, + "type": "integer" + }, + "type": "array" + }, + "slug": { + "description": "Unique identifying part for the web address of the dashboard.", + "maxLength": 255, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "tags": { + "items": { + "description": "Tags to be associated with the dashboard", + "nullable": true, + "type": "integer" + }, + "type": "array" + } + }, + "type": "object" + }, + "Database": { + "properties": { + "allow_multi_catalog": { + "type": "boolean" + }, + "allows_cost_estimate": { + "type": "boolean" + }, + "allows_subquery": { + "type": "boolean" + }, + "allows_virtual_table_explore": { + "type": "boolean" + }, + "backend": { + "type": "string" + }, + "disable_data_preview": { + "type": "boolean" + }, + "disable_drill_to_detail": { + "type": "boolean" + }, + "explore_database_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "Database1": { + "properties": { + "database_name": { + "type": "string" + } + }, + "type": "object" + }, + "DatabaseConnectionSchema": { + "properties": { + "allow_ctas": { + "description": "Allow CREATE TABLE AS option in SQL Lab", + "type": "boolean" + }, + "allow_cvas": { + "description": "Allow CREATE VIEW AS option in SQL Lab", + "type": "boolean" + }, + "allow_dml": { + "description": "Allow users to run non-SELECT statements (UPDATE, DELETE, CREATE, ...) in SQL Lab", + "type": "boolean" + }, + "allow_file_upload": { + "description": "Allow to upload CSV file data into this databaseIf selected, please set the schemas allowed for csv upload in Extra.", + "type": "boolean" + }, + "allow_run_async": { + "description": "Operate the database in asynchronous mode, meaning that the queries are executed on remote workers as opposed to on the web server itself. This assumes that you have a Celery worker setup as well as a results backend. Refer to the installation docs for more information.", + "type": "boolean" + }, + "backend": { + "description": "SQLAlchemy engine to use", + "nullable": true, + "type": "string" + }, + "cache_timeout": { + "description": "Duration (in seconds) of the caching timeout for charts of this database. A timeout of 0 indicates that the cache never expires. Note this defaults to the global timeout if undefined.", + "nullable": true, + "type": "integer" + }, + "configuration_method": { + "description": "Configuration_method is used on the frontend to inform the backend whether to explode parameters or to provide only a sqlalchemy_uri.", + "type": "string" + }, + "database_name": { + "description": "A database name to identify this connection.", + "maxLength": 250, + "minLength": 1, + "nullable": true, + "type": "string" + }, + "driver": { + "description": "SQLAlchemy driver to use", + "nullable": true, + "type": "string" + }, + "engine_information": { + "$ref": "#/components/schemas/EngineInformation" + }, + "expose_in_sqllab": { + "description": "Expose this database to SQLLab", + "type": "boolean" + }, + "extra": { + "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_file_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_file_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. The version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown.
6. The disable_data_preview field is a boolean specifying whether or not data preview queries will be run when fetching table metadata in SQL Lab.7. The disable_drill_to_detail field is a boolean specifying whether or notdrill to detail is disabled for the database.8. The allow_multi_catalog indicates if the database allows changing the default catalog when running queries and creating datasets.

", + "type": "string" + }, + "force_ctas_schema": { + "description": "When allowing CREATE TABLE AS option in SQL Lab, this option forces the table to be created in this schema", + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "id": { + "description": "Database ID (for updates)", + "type": "integer" + }, + "impersonate_user": { + "description": "If Presto, all the queries in SQL Lab are going to be executed as the currently logged on user who must have permission to run them.
If Hive and hive.server2.enable.doAs is enabled, will run the queries as service account, but impersonate the currently logged on user via hive.server2.proxy.user property.", + "type": "boolean" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "masked_encrypted_extra": { + "description": "

JSON string containing additional connection configuration.
This is used to provide connection information for systems like Hive, Presto, and BigQuery, which do not conform to the username:password syntax normally used by SQLAlchemy.

", + "nullable": true, + "type": "string" + }, + "parameters": { + "additionalProperties": {}, + "description": "DB-specific parameters for configuration", + "type": "object" + }, + "parameters_schema": { + "additionalProperties": {}, + "description": "JSONSchema for configuring the database by parameters instead of SQLAlchemy URI", + "type": "object" + }, + "server_cert": { + "description": "

Optional CA_BUNDLE contents to validate HTTPS requests. Only available on certain database engines.

", + "nullable": true, + "type": "string" + }, + "sqlalchemy_uri": { + "description": "

Refer to the SqlAlchemy docs for more information on how to structure your URI.

", + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "ssh_tunnel": { + "allOf": [ + { + "$ref": "#/components/schemas/DatabaseSSHTunnel" + } + ], + "nullable": true + }, + "uuid": { + "type": "string" + } + }, + "type": "object" + }, + "DatabaseFunctionNamesResponse": { + "properties": { + "function_names": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "DatabaseRelatedChart": { + "properties": { + "id": { + "type": "integer" + }, + "slice_name": { + "type": "string" + }, + "viz_type": { + "type": "string" + } + }, + "type": "object" + }, + "DatabaseRelatedCharts": { + "properties": { + "count": { + "description": "Chart count", + "type": "integer" + }, + "result": { + "description": "A list of dashboards", + "items": { + "$ref": "#/components/schemas/DatabaseRelatedChart" + }, + "type": "array" + } + }, + "type": "object" + }, + "DatabaseRelatedDashboard": { + "properties": { + "id": { + "type": "integer" + }, + "json_metadata": { + "type": "object" + }, + "slug": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "type": "object" + }, + "DatabaseRelatedDashboards": { + "properties": { + "count": { + "description": "Dashboard count", + "type": "integer" + }, + "result": { + "description": "A list of dashboards", + "items": { + "$ref": "#/components/schemas/DatabaseRelatedDashboard" + }, + "type": "array" + } + }, + "type": "object" + }, + "DatabaseRelatedObjectsResponse": { + "properties": { + "charts": { + "$ref": "#/components/schemas/DatabaseRelatedCharts" + }, + "dashboards": { + "$ref": "#/components/schemas/DatabaseRelatedDashboards" + } + }, + "type": "object" + }, + "DatabaseRestApi.get": { + "properties": { + "allow_ctas": { + "nullable": true, + "type": "boolean" + }, + "allow_cvas": { + "nullable": true, + "type": "boolean" + }, + "allow_dml": { + "nullable": true, + "type": "boolean" + }, + "allow_file_upload": { + "nullable": true, + "type": "boolean" + }, + "allow_run_async": { + "nullable": true, + "type": "boolean" + }, + "backend": { + "readOnly": true + }, + "cache_timeout": { + "nullable": true, + "type": "integer" + }, + "configuration_method": { + "maxLength": 255, + "nullable": true, + "type": "string" + }, + "database_name": { + "maxLength": 250, + "type": "string" + }, + "driver": { + "readOnly": true + }, + "engine_information": { + "readOnly": true + }, + "expose_in_sqllab": { + "nullable": true, + "type": "boolean" + }, + "force_ctas_schema": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "impersonate_user": { + "nullable": true, + "type": "boolean" + }, + "is_managed_externally": { + "type": "boolean" + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + } + }, + "required": [ + "database_name" + ], + "type": "object" + }, + "DatabaseRestApi.get_list": { + "properties": { + "allow_ctas": { + "nullable": true, + "type": "boolean" + }, + "allow_cvas": { + "nullable": true, + "type": "boolean" + }, + "allow_dml": { + "nullable": true, + "type": "boolean" + }, + "allow_file_upload": { + "nullable": true, + "type": "boolean" + }, + "allow_multi_catalog": { + "readOnly": true + }, + "allow_run_async": { + "nullable": true, + "type": "boolean" + }, + "allows_cost_estimate": { + "readOnly": true + }, + "allows_subquery": { + "readOnly": true + }, + "allows_virtual_table_explore": { + "readOnly": true + }, + "backend": { + "readOnly": true + }, + "changed_by": { + "$ref": "#/components/schemas/DatabaseRestApi.get_list.User" + }, + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/DatabaseRestApi.get_list.User1" + }, + "database_name": { + "maxLength": 250, + "type": "string" + }, + "disable_data_preview": { + "readOnly": true + }, + "disable_drill_to_detail": { + "readOnly": true + }, + "engine_information": { + "readOnly": true + }, + "explore_database_id": { + "readOnly": true + }, + "expose_in_sqllab": { + "nullable": true, + "type": "boolean" + }, + "extra": { + "nullable": true, + "type": "string" + }, + "force_ctas_schema": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + } + }, + "required": [ + "database_name" + ], + "type": "object" + }, + "DatabaseRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DatabaseRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DatabaseRestApi.post": { + "properties": { + "allow_ctas": { + "description": "Allow CREATE TABLE AS option in SQL Lab", + "type": "boolean" + }, + "allow_cvas": { + "description": "Allow CREATE VIEW AS option in SQL Lab", + "type": "boolean" + }, + "allow_dml": { + "description": "Allow users to run non-SELECT statements (UPDATE, DELETE, CREATE, ...) in SQL Lab", + "type": "boolean" + }, + "allow_file_upload": { + "description": "Allow to upload CSV file data into this databaseIf selected, please set the schemas allowed for csv upload in Extra.", + "type": "boolean" + }, + "allow_run_async": { + "description": "Operate the database in asynchronous mode, meaning that the queries are executed on remote workers as opposed to on the web server itself. This assumes that you have a Celery worker setup as well as a results backend. Refer to the installation docs for more information.", + "type": "boolean" + }, + "cache_timeout": { + "description": "Duration (in seconds) of the caching timeout for charts of this database. A timeout of 0 indicates that the cache never expires. Note this defaults to the global timeout if undefined.", + "nullable": true, + "type": "integer" + }, + "configuration_method": { + "default": "sqlalchemy_form", + "description": "Configuration_method is used on the frontend to inform the backend whether to explode parameters or to provide only a sqlalchemy_uri.", + "enum": [ + "sqlalchemy_form", + "dynamic_form" + ] + }, + "database_name": { + "description": "A database name to identify this connection.", + "maxLength": 250, + "minLength": 1, + "type": "string" + }, + "driver": { + "description": "SQLAlchemy driver to use", + "nullable": true, + "type": "string" + }, + "engine": { + "description": "SQLAlchemy engine to use", + "nullable": true, + "type": "string" + }, + "expose_in_sqllab": { + "description": "Expose this database to SQLLab", + "type": "boolean" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "extra": { + "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_file_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_file_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. The version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown.
6. The disable_data_preview field is a boolean specifying whether or not data preview queries will be run when fetching table metadata in SQL Lab.7. The disable_drill_to_detail field is a boolean specifying whether or notdrill to detail is disabled for the database.8. The allow_multi_catalog indicates if the database allows changing the default catalog when running queries and creating datasets.

", + "type": "string" + }, + "force_ctas_schema": { + "description": "When allowing CREATE TABLE AS option in SQL Lab, this option forces the table to be created in this schema", + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "impersonate_user": { + "description": "If Presto, all the queries in SQL Lab are going to be executed as the currently logged on user who must have permission to run them.
If Hive and hive.server2.enable.doAs is enabled, will run the queries as service account, but impersonate the currently logged on user via hive.server2.proxy.user property.", + "type": "boolean" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "masked_encrypted_extra": { + "description": "

JSON string containing additional connection configuration.
This is used to provide connection information for systems like Hive, Presto, and BigQuery, which do not conform to the username:password syntax normally used by SQLAlchemy.

", + "nullable": true, + "type": "string" + }, + "parameters": { + "additionalProperties": {}, + "description": "DB-specific parameters for configuration", + "type": "object" + }, + "server_cert": { + "description": "

Optional CA_BUNDLE contents to validate HTTPS requests. Only available on certain database engines.

", + "nullable": true, + "type": "string" + }, + "sqlalchemy_uri": { + "description": "

Refer to the SqlAlchemy docs for more information on how to structure your URI.

", + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "ssh_tunnel": { + "allOf": [ + { + "$ref": "#/components/schemas/DatabaseSSHTunnel" + } + ], + "nullable": true + }, + "uuid": { + "type": "string" + } + }, + "required": [ + "database_name" + ], + "type": "object" + }, + "DatabaseRestApi.put": { + "properties": { + "allow_ctas": { + "description": "Allow CREATE TABLE AS option in SQL Lab", + "type": "boolean" + }, + "allow_cvas": { + "description": "Allow CREATE VIEW AS option in SQL Lab", + "type": "boolean" + }, + "allow_dml": { + "description": "Allow users to run non-SELECT statements (UPDATE, DELETE, CREATE, ...) in SQL Lab", + "type": "boolean" + }, + "allow_file_upload": { + "description": "Allow to upload CSV file data into this databaseIf selected, please set the schemas allowed for csv upload in Extra.", + "type": "boolean" + }, + "allow_run_async": { + "description": "Operate the database in asynchronous mode, meaning that the queries are executed on remote workers as opposed to on the web server itself. This assumes that you have a Celery worker setup as well as a results backend. Refer to the installation docs for more information.", + "type": "boolean" + }, + "cache_timeout": { + "description": "Duration (in seconds) of the caching timeout for charts of this database. A timeout of 0 indicates that the cache never expires. Note this defaults to the global timeout if undefined.", + "nullable": true, + "type": "integer" + }, + "configuration_method": { + "default": "sqlalchemy_form", + "description": "Configuration_method is used on the frontend to inform the backend whether to explode parameters or to provide only a sqlalchemy_uri.", + "enum": [ + "sqlalchemy_form", + "dynamic_form" + ] + }, + "database_name": { + "description": "A database name to identify this connection.", + "maxLength": 250, + "minLength": 1, + "nullable": true, + "type": "string" + }, + "driver": { + "description": "SQLAlchemy driver to use", + "nullable": true, + "type": "string" + }, + "engine": { + "description": "SQLAlchemy engine to use", + "nullable": true, + "type": "string" + }, + "expose_in_sqllab": { + "description": "Expose this database to SQLLab", + "type": "boolean" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "extra": { + "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_file_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_file_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. The version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown.
6. The disable_data_preview field is a boolean specifying whether or not data preview queries will be run when fetching table metadata in SQL Lab.7. The disable_drill_to_detail field is a boolean specifying whether or notdrill to detail is disabled for the database.8. The allow_multi_catalog indicates if the database allows changing the default catalog when running queries and creating datasets.

", + "type": "string" + }, + "force_ctas_schema": { + "description": "When allowing CREATE TABLE AS option in SQL Lab, this option forces the table to be created in this schema", + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "impersonate_user": { + "description": "If Presto, all the queries in SQL Lab are going to be executed as the currently logged on user who must have permission to run them.
If Hive and hive.server2.enable.doAs is enabled, will run the queries as service account, but impersonate the currently logged on user via hive.server2.proxy.user property.", + "type": "boolean" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "masked_encrypted_extra": { + "description": "

JSON string containing additional connection configuration.
This is used to provide connection information for systems like Hive, Presto, and BigQuery, which do not conform to the username:password syntax normally used by SQLAlchemy.

", + "nullable": true, + "type": "string" + }, + "parameters": { + "additionalProperties": {}, + "description": "DB-specific parameters for configuration", + "type": "object" + }, + "server_cert": { + "description": "

Optional CA_BUNDLE contents to validate HTTPS requests. Only available on certain database engines.

", + "nullable": true, + "type": "string" + }, + "sqlalchemy_uri": { + "description": "

Refer to the SqlAlchemy docs for more information on how to structure your URI.

", + "maxLength": 1024, + "minLength": 0, + "type": "string" + }, + "ssh_tunnel": { + "allOf": [ + { + "$ref": "#/components/schemas/DatabaseSSHTunnel" + } + ], + "nullable": true + }, + "uuid": { + "type": "string" + } + }, + "type": "object" + }, + "DatabaseSSHTunnel": { + "properties": { + "id": { + "description": "SSH Tunnel ID (for updates)", + "nullable": true, + "type": "integer" + }, + "password": { + "type": "string" + }, + "private_key": { + "type": "string" + }, + "private_key_password": { + "type": "string" + }, + "server_address": { + "type": "string" + }, + "server_port": { + "type": "integer" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "DatabaseSchemaAccessForFileUploadResponse": { + "properties": { + "schemas": { + "description": "The list of schemas allowed for the database to upload information", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "DatabaseTablesResponse": { + "properties": { + "extra": { + "description": "Extra data used to specify column metadata", + "type": "object" + }, + "type": { + "description": "table or view", + "type": "string" + }, + "value": { + "description": "The table or view name", + "type": "string" + } + }, + "type": "object" + }, + "DatabaseTestConnectionSchema": { + "properties": { + "configuration_method": { + "default": "sqlalchemy_form", + "description": "Configuration_method is used on the frontend to inform the backend whether to explode parameters or to provide only a sqlalchemy_uri.", + "enum": [ + "sqlalchemy_form", + "dynamic_form" + ] + }, + "database_name": { + "description": "A database name to identify this connection.", + "maxLength": 250, + "minLength": 1, + "nullable": true, + "type": "string" + }, + "driver": { + "description": "SQLAlchemy driver to use", + "nullable": true, + "type": "string" + }, + "engine": { + "description": "SQLAlchemy engine to use", + "nullable": true, + "type": "string" + }, + "extra": { + "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_file_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_file_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. The version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown.
6. The disable_data_preview field is a boolean specifying whether or not data preview queries will be run when fetching table metadata in SQL Lab.7. The disable_drill_to_detail field is a boolean specifying whether or notdrill to detail is disabled for the database.8. The allow_multi_catalog indicates if the database allows changing the default catalog when running queries and creating datasets.

", + "type": "string" + }, + "impersonate_user": { + "description": "If Presto, all the queries in SQL Lab are going to be executed as the currently logged on user who must have permission to run them.
If Hive and hive.server2.enable.doAs is enabled, will run the queries as service account, but impersonate the currently logged on user via hive.server2.proxy.user property.", + "type": "boolean" + }, + "masked_encrypted_extra": { + "description": "

JSON string containing additional connection configuration.
This is used to provide connection information for systems like Hive, Presto, and BigQuery, which do not conform to the username:password syntax normally used by SQLAlchemy.

", + "nullable": true, + "type": "string" + }, + "parameters": { + "additionalProperties": {}, + "description": "DB-specific parameters for configuration", + "type": "object" + }, + "server_cert": { + "description": "

Optional CA_BUNDLE contents to validate HTTPS requests. Only available on certain database engines.

", + "nullable": true, + "type": "string" + }, + "sqlalchemy_uri": { + "description": "

Refer to the SqlAlchemy docs for more information on how to structure your URI.

", + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "ssh_tunnel": { + "allOf": [ + { + "$ref": "#/components/schemas/DatabaseSSHTunnel" + } + ], + "nullable": true + } + }, + "type": "object" + }, + "DatabaseValidateParametersSchema": { + "properties": { + "catalog": { + "additionalProperties": { + "nullable": true + }, + "description": "Gsheets specific column for managing label to sheet urls", + "type": "object" + }, + "configuration_method": { + "description": "Configuration_method is used on the frontend to inform the backend whether to explode parameters or to provide only a sqlalchemy_uri.", + "enum": [ + "sqlalchemy_form", + "dynamic_form" + ] + }, + "database_name": { + "description": "A database name to identify this connection.", + "maxLength": 250, + "minLength": 1, + "nullable": true, + "type": "string" + }, + "driver": { + "description": "SQLAlchemy driver to use", + "nullable": true, + "type": "string" + }, + "engine": { + "description": "SQLAlchemy engine to use", + "type": "string" + }, + "extra": { + "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_file_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_file_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. The version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown.
6. The disable_data_preview field is a boolean specifying whether or not data preview queries will be run when fetching table metadata in SQL Lab.7. The disable_drill_to_detail field is a boolean specifying whether or notdrill to detail is disabled for the database.8. The allow_multi_catalog indicates if the database allows changing the default catalog when running queries and creating datasets.

", + "type": "string" + }, + "id": { + "description": "Database ID (for updates)", + "nullable": true, + "type": "integer" + }, + "impersonate_user": { + "description": "If Presto, all the queries in SQL Lab are going to be executed as the currently logged on user who must have permission to run them.
If Hive and hive.server2.enable.doAs is enabled, will run the queries as service account, but impersonate the currently logged on user via hive.server2.proxy.user property.", + "type": "boolean" + }, + "masked_encrypted_extra": { + "description": "

JSON string containing additional connection configuration.
This is used to provide connection information for systems like Hive, Presto, and BigQuery, which do not conform to the username:password syntax normally used by SQLAlchemy.

", + "nullable": true, + "type": "string" + }, + "parameters": { + "additionalProperties": { + "nullable": true + }, + "description": "DB-specific parameters for configuration", + "type": "object" + }, + "server_cert": { + "description": "

Optional CA_BUNDLE contents to validate HTTPS requests. Only available on certain database engines.

", + "nullable": true, + "type": "string" + } + }, + "required": [ + "configuration_method", + "engine" + ], + "type": "object" + }, + "Dataset": { + "properties": { + "cache_timeout": { + "description": "Duration (in seconds) of the caching timeout for this dataset.", + "type": "integer" + }, + "column_formats": { + "description": "Column formats.", + "type": "object" + }, + "columns": { + "description": "Columns metadata.", + "items": { + "type": "object" + }, + "type": "array" + }, + "database": { + "description": "Database associated with the dataset.", + "type": "object" + }, + "datasource_name": { + "description": "Dataset name.", + "type": "string" + }, + "default_endpoint": { + "description": "Default endpoint for the dataset.", + "type": "string" + }, + "description": { + "description": "Dataset description.", + "type": "string" + }, + "edit_url": { + "description": "The URL for editing the dataset.", + "type": "string" + }, + "extra": { + "description": "JSON string containing extra configuration elements.", + "type": "object" + }, + "fetch_values_predicate": { + "description": "Predicate used when fetching values from the dataset.", + "type": "string" + }, + "filter_select": { + "description": "SELECT filter applied to the dataset.", + "type": "boolean" + }, + "filter_select_enabled": { + "description": "If the SELECT filter is enabled.", + "type": "boolean" + }, + "granularity_sqla": { + "description": "Name of temporal column used for time filtering for SQL datasources. This field is deprecated, use `granularity` instead.", + "items": { + "items": { + "type": "object" + }, + "type": "array" + }, + "type": "array" + }, + "health_check_message": { + "description": "Health check message.", + "type": "string" + }, + "id": { + "description": "Dataset ID.", + "type": "integer" + }, + "is_sqllab_view": { + "description": "If the dataset is a SQL Lab view.", + "type": "boolean" + }, + "main_dttm_col": { + "description": "The main temporal column.", + "type": "string" + }, + "metrics": { + "description": "Dataset metrics.", + "items": { + "type": "object" + }, + "type": "array" + }, + "name": { + "description": "Dataset name.", + "type": "string" + }, + "offset": { + "description": "Dataset offset.", + "type": "integer" + }, + "order_by_choices": { + "description": "List of order by columns.", + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "array" + }, + "owners": { + "description": "List of owners identifiers", + "items": { + "type": "integer" + }, + "type": "array" + }, + "params": { + "description": "Extra params for the dataset.", + "type": "object" + }, + "perm": { + "description": "Permission expression.", + "type": "string" + }, + "schema": { + "description": "Dataset schema.", + "type": "string" + }, + "select_star": { + "description": "Select all clause.", + "type": "string" + }, + "sql": { + "description": "A SQL statement that defines the dataset.", + "type": "string" + }, + "table_name": { + "description": "The name of the table associated with the dataset.", + "type": "string" + }, + "template_params": { + "description": "Table template params.", + "type": "object" + }, + "time_grain_sqla": { + "description": "List of temporal granularities supported by the dataset.", + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "array" + }, + "type": { + "description": "Dataset type.", + "type": "string" + }, + "uid": { + "description": "Dataset unique identifier.", + "type": "string" + }, + "verbose_map": { + "description": "Mapping from raw name to verbose name.", + "type": "object" + } + }, + "type": "object" + }, + "DatasetCacheWarmUpRequestSchema": { + "properties": { + "dashboard_id": { + "description": "The ID of the dashboard to get filters for when warming cache", + "type": "integer" + }, + "db_name": { + "description": "The name of the database where the table is located", + "type": "string" + }, + "extra_filters": { + "description": "Extra filters to apply when warming up cache", + "type": "string" + }, + "table_name": { + "description": "The name of the table to warm up cache for", + "type": "string" + } + }, + "required": [ + "db_name", + "table_name" + ], + "type": "object" + }, + "DatasetCacheWarmUpResponseSchema": { + "properties": { + "result": { + "description": "A list of each chart's warmup status and errors if any", + "items": { + "$ref": "#/components/schemas/DatasetCacheWarmUpResponseSingle" + }, + "type": "array" + } + }, + "type": "object" + }, + "DatasetCacheWarmUpResponseSingle": { + "properties": { + "chart_id": { + "description": "The ID of the chart the status belongs to", + "type": "integer" + }, + "viz_error": { + "description": "Error that occurred when warming cache for chart", + "type": "string" + }, + "viz_status": { + "description": "Status of the underlying query for the viz", + "type": "string" + } + }, + "type": "object" + }, + "DatasetColumnsPut": { + "properties": { + "advanced_data_type": { + "maxLength": 255, + "minLength": 1, + "nullable": true, + "type": "string" + }, + "column_name": { + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "description": { + "nullable": true, + "type": "string" + }, + "expression": { + "nullable": true, + "type": "string" + }, + "extra": { + "nullable": true, + "type": "string" + }, + "filterable": { + "type": "boolean" + }, + "groupby": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "is_active": { + "nullable": true, + "type": "boolean" + }, + "is_dttm": { + "nullable": true, + "type": "boolean" + }, + "python_date_format": { + "maxLength": 255, + "minLength": 1, + "nullable": true, + "type": "string" + }, + "type": { + "nullable": true, + "type": "string" + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + }, + "verbose_name": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "column_name" + ], + "type": "object" + }, + "DatasetColumnsRestApi.get": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "DatasetColumnsRestApi.get_list": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "DatasetColumnsRestApi.post": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "DatasetColumnsRestApi.put": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "DatasetDuplicateSchema": { + "properties": { + "base_model_id": { + "type": "integer" + }, + "table_name": { + "maxLength": 250, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "base_model_id", + "table_name" + ], + "type": "object" + }, + "DatasetMetricRestApi.get": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "DatasetMetricRestApi.get_list": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "DatasetMetricRestApi.post": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "DatasetMetricRestApi.put": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "DatasetMetricsPut": { + "properties": { + "currency": { + "maxLength": 128, + "minLength": 1, + "nullable": true, + "type": "string" + }, + "d3format": { + "maxLength": 128, + "minLength": 1, + "nullable": true, + "type": "string" + }, + "description": { + "nullable": true, + "type": "string" + }, + "expression": { + "type": "string" + }, + "extra": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "metric_name": { + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "metric_type": { + "maxLength": 32, + "minLength": 1, + "nullable": true, + "type": "string" + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + }, + "verbose_name": { + "nullable": true, + "type": "string" + }, + "warning_text": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "expression", + "metric_name" + ], + "type": "object" + }, + "DatasetRelatedChart": { + "properties": { + "id": { + "type": "integer" + }, + "slice_name": { + "type": "string" + }, + "viz_type": { + "type": "string" + } + }, + "type": "object" + }, + "DatasetRelatedCharts": { + "properties": { + "count": { + "description": "Chart count", + "type": "integer" + }, + "result": { + "description": "A list of dashboards", + "items": { + "$ref": "#/components/schemas/DatasetRelatedChart" + }, + "type": "array" + } + }, + "type": "object" + }, + "DatasetRelatedDashboard": { + "properties": { + "id": { + "type": "integer" + }, + "json_metadata": { + "type": "object" + }, + "slug": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "type": "object" + }, + "DatasetRelatedDashboards": { + "properties": { + "count": { + "description": "Dashboard count", + "type": "integer" + }, + "result": { + "description": "A list of dashboards", + "items": { + "$ref": "#/components/schemas/DatasetRelatedDashboard" + }, + "type": "array" + } + }, + "type": "object" + }, + "DatasetRelatedObjectsResponse": { + "properties": { + "charts": { + "$ref": "#/components/schemas/DatasetRelatedCharts" + }, + "dashboards": { + "$ref": "#/components/schemas/DatasetRelatedDashboards" + } + }, + "type": "object" + }, + "DatasetRestApi.get": { + "properties": { + "always_filter_main_dttm": { + "nullable": true, + "type": "boolean" + }, + "cache_timeout": { + "nullable": true, + "type": "integer" + }, + "catalog": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "changed_by": { + "$ref": "#/components/schemas/DatasetRestApi.get.User2" + }, + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "changed_on_humanized": { + "readOnly": true + }, + "column_formats": { + "readOnly": true + }, + "columns": { + "$ref": "#/components/schemas/DatasetRestApi.get.TableColumn" + }, + "created_by": { + "$ref": "#/components/schemas/DatasetRestApi.get.User1" + }, + "created_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "created_on_humanized": { + "readOnly": true + }, + "database": { + "$ref": "#/components/schemas/DatasetRestApi.get.Database" + }, + "datasource_name": { + "readOnly": true + }, + "datasource_type": { + "readOnly": true + }, + "default_endpoint": { + "nullable": true, + "type": "string" + }, + "description": { + "nullable": true, + "type": "string" + }, + "extra": { + "nullable": true, + "type": "string" + }, + "fetch_values_predicate": { + "nullable": true, + "type": "string" + }, + "filter_select_enabled": { + "nullable": true, + "type": "boolean" + }, + "folders": { + "nullable": true + }, + "granularity_sqla": { + "readOnly": true + }, + "id": { + "type": "integer" + }, + "is_managed_externally": { + "type": "boolean" + }, + "is_sqllab_view": { + "nullable": true, + "type": "boolean" + }, + "kind": { + "readOnly": true + }, + "main_dttm_col": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "metrics": { + "$ref": "#/components/schemas/DatasetRestApi.get.SqlMetric" + }, + "name": { + "readOnly": true + }, + "normalize_columns": { + "nullable": true, + "type": "boolean" + }, + "offset": { + "nullable": true, + "type": "integer" + }, + "order_by_choices": { + "readOnly": true + }, + "owners": { + "$ref": "#/components/schemas/DatasetRestApi.get.User" + }, + "schema": { + "maxLength": 255, + "nullable": true, + "type": "string" + }, + "select_star": { + "readOnly": true + }, + "sql": { + "nullable": true, + "type": "string" + }, + "table_name": { + "maxLength": 250, + "type": "string" + }, + "template_params": { + "nullable": true, + "type": "string" + }, + "time_grain_sqla": { + "readOnly": true + }, + "uid": { + "readOnly": true + }, + "url": { + "readOnly": true + }, + "verbose_map": { + "readOnly": true + } + }, + "required": [ + "columns", + "database", + "metrics", + "table_name" + ], + "type": "object" + }, + "DatasetRestApi.get.Database": { + "properties": { + "allow_multi_catalog": { + "readOnly": true + }, + "backend": { + "readOnly": true + }, + "database_name": { + "maxLength": 250, + "type": "string" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "database_name" + ], + "type": "object" + }, + "DatasetRestApi.get.SqlMetric": { + "properties": { + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "created_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "currency": { + "nullable": true + }, + "d3format": { + "maxLength": 128, + "nullable": true, + "type": "string" + }, + "description": { + "nullable": true, + "type": "string" + }, + "expression": { + "type": "string" + }, + "extra": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "metric_name": { + "maxLength": 255, + "type": "string" + }, + "metric_type": { + "maxLength": 32, + "nullable": true, + "type": "string" + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + }, + "verbose_name": { + "maxLength": 1024, + "nullable": true, + "type": "string" + }, + "warning_text": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "expression", + "metric_name" + ], + "type": "object" + }, + "DatasetRestApi.get.TableColumn": { + "properties": { + "advanced_data_type": { + "maxLength": 255, + "nullable": true, + "type": "string" + }, + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "column_name": { + "maxLength": 255, + "type": "string" + }, + "created_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "description": { + "nullable": true, + "type": "string" + }, + "expression": { + "nullable": true, + "type": "string" + }, + "extra": { + "nullable": true, + "type": "string" + }, + "filterable": { + "nullable": true, + "type": "boolean" + }, + "groupby": { + "nullable": true, + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "is_active": { + "nullable": true, + "type": "boolean" + }, + "is_dttm": { + "nullable": true, + "type": "boolean" + }, + "python_date_format": { + "maxLength": 255, + "nullable": true, + "type": "string" + }, + "type": { + "nullable": true, + "type": "string" + }, + "type_generic": { + "readOnly": true + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + }, + "verbose_name": { + "maxLength": 1024, + "nullable": true, + "type": "string" + } + }, + "required": [ + "column_name" + ], + "type": "object" + }, + "DatasetRestApi.get.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DatasetRestApi.get.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DatasetRestApi.get.User2": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DatasetRestApi.get_list": { + "properties": { + "catalog": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "changed_by": { + "$ref": "#/components/schemas/DatasetRestApi.get_list.User" + }, + "changed_by_name": { + "readOnly": true + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "changed_on_utc": { + "readOnly": true + }, + "database": { + "$ref": "#/components/schemas/DatasetRestApi.get_list.Database" + }, + "datasource_type": { + "readOnly": true + }, + "default_endpoint": { + "nullable": true, + "type": "string" + }, + "description": { + "nullable": true, + "type": "string" + }, + "explore_url": { + "readOnly": true + }, + "extra": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "kind": { + "readOnly": true + }, + "owners": { + "$ref": "#/components/schemas/DatasetRestApi.get_list.User1" + }, + "schema": { + "maxLength": 255, + "nullable": true, + "type": "string" + }, + "sql": { + "nullable": true, + "type": "string" + }, + "table_name": { + "maxLength": 250, + "type": "string" + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + } + }, + "required": [ + "database", + "table_name" + ], + "type": "object" + }, + "DatasetRestApi.get_list.Database": { + "properties": { + "database_name": { + "maxLength": 250, + "type": "string" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "database_name" + ], + "type": "object" + }, + "DatasetRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DatasetRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DatasetRestApi.post": { + "properties": { + "always_filter_main_dttm": { + "default": false, + "type": "boolean" + }, + "catalog": { + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "database": { + "type": "integer" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "normalize_columns": { + "default": false, + "type": "boolean" + }, + "owners": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "schema": { + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "sql": { + "nullable": true, + "type": "string" + }, + "table_name": { + "maxLength": 250, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "database", + "table_name" + ], + "type": "object" + }, + "DatasetRestApi.put": { + "properties": { + "always_filter_main_dttm": { + "default": false, + "type": "boolean" + }, + "cache_timeout": { + "nullable": true, + "type": "integer" + }, + "catalog": { + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "columns": { + "items": { + "$ref": "#/components/schemas/DatasetColumnsPut" + }, + "type": "array" + }, + "database_id": { + "type": "integer" + }, + "default_endpoint": { + "nullable": true, + "type": "string" + }, + "description": { + "nullable": true, + "type": "string" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "extra": { + "nullable": true, + "type": "string" + }, + "fetch_values_predicate": { + "maxLength": 1000, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "filter_select_enabled": { + "nullable": true, + "type": "boolean" + }, + "folders": { + "items": { + "$ref": "#/components/schemas/Folder" + }, + "type": "array" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "is_sqllab_view": { + "nullable": true, + "type": "boolean" + }, + "main_dttm_col": { + "nullable": true, + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/components/schemas/DatasetMetricsPut" + }, + "type": "array" + }, + "normalize_columns": { + "nullable": true, + "type": "boolean" + }, + "offset": { + "nullable": true, + "type": "integer" + }, + "owners": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "schema": { + "maxLength": 255, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "sql": { + "nullable": true, + "type": "string" + }, + "table_name": { + "maxLength": 250, + "minLength": 1, + "nullable": true, + "type": "string" + }, + "template_params": { + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "Datasource": { + "properties": { + "catalog": { + "description": "Datasource catalog", + "nullable": true, + "type": "string" + }, + "database_name": { + "description": "Datasource name", + "type": "string" + }, + "datasource_name": { + "description": "The datasource name.", + "type": "string" + }, + "datasource_type": { + "description": "The type of dataset/datasource identified on `datasource_id`.", + "enum": [ + "table", + "dataset", + "query", + "saved_query", + "view" + ], + "type": "string" + }, + "schema": { + "description": "Datasource schema", + "type": "string" + } + }, + "required": [ + "datasource_type" + ], + "type": "object" + }, + "DistincResponseSchema": { + "properties": { + "count": { + "description": "The total number of distinct values", + "type": "integer" + }, + "result": { + "items": { + "$ref": "#/components/schemas/DistinctResultResponse" + }, + "type": "array" + } + }, + "type": "object" + }, + "DistinctResultResponse": { + "properties": { + "text": { + "description": "The distinct item", + "type": "string" + } + }, + "type": "object" + }, + "EmbeddedDashboardConfig": { + "properties": { + "allowed_domains": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "allowed_domains" + ], + "type": "object" + }, + "EmbeddedDashboardResponseSchema": { + "properties": { + "allowed_domains": { + "items": { + "type": "string" + }, + "type": "array" + }, + "changed_by": { + "$ref": "#/components/schemas/User1" + }, + "changed_on": { + "format": "date-time", + "type": "string" + }, + "dashboard_id": { + "type": "string" + }, + "uuid": { + "type": "string" + } + }, + "type": "object" + }, + "EmbeddedDashboardRestApi.get": { + "properties": { + "uuid": { + "format": "uuid", + "type": "string" + } + }, + "type": "object" + }, + "EmbeddedDashboardRestApi.get_list": { + "properties": { + "uuid": { + "format": "uuid", + "type": "string" + } + }, + "type": "object" + }, + "EmbeddedDashboardRestApi.post": { + "properties": { + "uuid": { + "format": "uuid", + "type": "string" + } + }, + "type": "object" + }, + "EmbeddedDashboardRestApi.put": { + "properties": { + "uuid": { + "format": "uuid", + "type": "string" + } + }, + "type": "object" + }, + "EngineInformation": { + "properties": { + "disable_ssh_tunneling": { + "description": "SSH tunnel is not available to the database", + "type": "boolean" + }, + "supports_dynamic_catalog": { + "description": "The database supports multiple catalogs in a single connection", + "type": "boolean" + }, + "supports_file_upload": { + "description": "Users can upload files to the database", + "type": "boolean" + }, + "supports_oauth2": { + "description": "The database supports OAuth2", + "type": "boolean" + } + }, + "type": "object" + }, + "EstimateQueryCostSchema": { + "properties": { + "catalog": { + "description": "The database catalog", + "nullable": true, + "type": "string" + }, + "database_id": { + "description": "The database id", + "type": "integer" + }, + "schema": { + "description": "The database schema", + "nullable": true, + "type": "string" + }, + "sql": { + "description": "The SQL query to estimate", + "type": "string" + }, + "template_params": { + "description": "The SQL query template params", + "type": "object" + } + }, + "required": [ + "database_id", + "sql" + ], + "type": "object" + }, + "ExecutePayloadSchema": { + "properties": { + "catalog": { + "nullable": true, + "type": "string" + }, + "client_id": { + "nullable": true, + "type": "string" + }, + "ctas_method": { + "nullable": true, + "type": "string" + }, + "database_id": { + "type": "integer" + }, + "expand_data": { + "nullable": true, + "type": "boolean" + }, + "json": { + "nullable": true, + "type": "boolean" + }, + "queryLimit": { + "nullable": true, + "type": "integer" + }, + "runAsync": { + "nullable": true, + "type": "boolean" + }, + "schema": { + "nullable": true, + "type": "string" + }, + "select_as_cta": { + "nullable": true, + "type": "boolean" + }, + "sql": { + "type": "string" + }, + "sql_editor_id": { + "nullable": true, + "type": "string" + }, + "tab": { + "nullable": true, + "type": "string" + }, + "templateParams": { + "nullable": true, + "type": "string" + }, + "tmp_table_name": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "database_id", + "sql" + ], + "type": "object" + }, + "ExploreContextSchema": { + "properties": { + "dataset": { + "$ref": "#/components/schemas/Dataset" + }, + "form_data": { + "description": "Form data from the Explore controls used to form the chart's data query.", + "type": "object" + }, + "message": { + "description": "Any message related to the processed request.", + "type": "string" + }, + "slice": { + "$ref": "#/components/schemas/Slice" + } + }, + "type": "object" + }, + "ExplorePermalinkStateSchema": { + "properties": { + "formData": { + "description": "Chart form data", + "type": "object" + }, + "urlParams": { + "description": "URL Parameters", + "items": { + "description": "URL Parameter key-value pair", + "nullable": true + }, + "nullable": true, + "type": "array" + } + }, + "required": [ + "formData" + ], + "type": "object" + }, + "Folder": { + "properties": { + "children": { + "items": { + "$ref": "#/components/schemas/Folder" + }, + "nullable": true, + "type": "array" + }, + "description": { + "maxLength": 1000, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "name": { + "maxLength": 250, + "minLength": 1, + "type": "string" + }, + "type": { + "enum": [ + "metric", + "column", + "folder" + ], + "type": "string" + }, + "uuid": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "uuid" + ], + "type": "object" + }, + "FormDataPostSchema": { + "properties": { + "chart_id": { + "description": "The chart ID", + "type": "integer" + }, + "datasource_id": { + "description": "The datasource ID", + "type": "integer" + }, + "datasource_type": { + "description": "The datasource type", + "enum": [ + "table", + "dataset", + "query", + "saved_query", + "view" + ], + "type": "string" + }, + "form_data": { + "description": "Any type of JSON supported text.", + "type": "string" + } + }, + "required": [ + "datasource_id", + "datasource_type", + "form_data" + ], + "type": "object" + }, + "FormDataPutSchema": { + "properties": { + "chart_id": { + "description": "The chart ID", + "type": "integer" + }, + "datasource_id": { + "description": "The datasource ID", + "type": "integer" + }, + "datasource_type": { + "description": "The datasource type", + "enum": [ + "table", + "dataset", + "query", + "saved_query", + "view" + ], + "type": "string" + }, + "form_data": { + "description": "Any type of JSON supported text.", + "type": "string" + } + }, + "required": [ + "datasource_id", + "datasource_type", + "form_data" + ], + "type": "object" + }, + "GetFavStarIdsSchema": { + "properties": { + "result": { + "description": "A list of results for each corresponding chart in the request", + "items": { + "$ref": "#/components/schemas/ChartFavStarResponseResult" + }, + "type": "array" + } + }, + "type": "object" + }, + "GetOrCreateDatasetSchema": { + "properties": { + "always_filter_main_dttm": { + "default": false, + "type": "boolean" + }, + "catalog": { + "description": "The catalog the table belongs to", + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "database_id": { + "description": "ID of database table belongs to", + "type": "integer" + }, + "normalize_columns": { + "default": false, + "type": "boolean" + }, + "schema": { + "description": "The schema the table belongs to", + "maxLength": 250, + "minLength": 0, + "nullable": true, + "type": "string" + }, + "table_name": { + "description": "Name of table", + "type": "string" + }, + "template_params": { + "description": "Template params for the table", + "type": "string" + } + }, + "required": [ + "database_id", + "table_name" + ], + "type": "object" + }, + "GuestTokenCreate": { + "properties": { + "resources": { + "items": { + "$ref": "#/components/schemas/Resource" + }, + "type": "array" + }, + "rls": { + "items": { + "$ref": "#/components/schemas/RlsRule" + }, + "type": "array" + }, + "user": { + "$ref": "#/components/schemas/User2" + } + }, + "required": [ + "resources", + "rls" + ], + "type": "object" + }, + "ImportV1Database": { + "properties": { + "allow_csv_upload": { + "type": "boolean" + }, + "allow_ctas": { + "type": "boolean" + }, + "allow_cvas": { + "type": "boolean" + }, + "allow_dml": { + "type": "boolean" + }, + "allow_run_async": { + "type": "boolean" + }, + "cache_timeout": { + "nullable": true, + "type": "integer" + }, + "database_name": { + "type": "string" + }, + "encrypted_extra": { + "nullable": true, + "type": "string" + }, + "expose_in_sqllab": { + "type": "boolean" + }, + "external_url": { + "nullable": true, + "type": "string" + }, + "extra": { + "$ref": "#/components/schemas/ImportV1DatabaseExtra" + }, + "impersonate_user": { + "type": "boolean" + }, + "is_managed_externally": { + "nullable": true, + "type": "boolean" + }, + "password": { + "nullable": true, + "type": "string" + }, + "sqlalchemy_uri": { + "type": "string" + }, + "ssh_tunnel": { + "allOf": [ + { + "$ref": "#/components/schemas/DatabaseSSHTunnel" + } + ], + "nullable": true + }, + "uuid": { + "format": "uuid", + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "database_name", + "sqlalchemy_uri", + "uuid", + "version" + ], + "type": "object" + }, + "ImportV1DatabaseExtra": { + "properties": { + "allow_multi_catalog": { + "type": "boolean" + }, + "allows_virtual_table_explore": { + "type": "boolean" + }, + "cancel_query_on_windows_unload": { + "type": "boolean" + }, + "cost_estimate_enabled": { + "type": "boolean" + }, + "disable_data_preview": { + "type": "boolean" + }, + "disable_drill_to_detail": { + "type": "boolean" + }, + "engine_params": { + "additionalProperties": {}, + "type": "object" + }, + "metadata_cache_timeout": { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + "metadata_params": { + "additionalProperties": {}, + "type": "object" + }, + "schema_options": { + "additionalProperties": {}, + "type": "object" + }, + "schemas_allowed_for_csv_upload": { + "items": { + "type": "string" + }, + "type": "array" + }, + "version": { + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "LogRestApi.get": { + "properties": { + "action": { + "maxLength": 512, + "nullable": true, + "type": "string" + }, + "dashboard_id": { + "nullable": true, + "type": "integer" + }, + "dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "duration_ms": { + "nullable": true, + "type": "integer" + }, + "json": { + "nullable": true, + "type": "string" + }, + "referrer": { + "maxLength": 1024, + "nullable": true, + "type": "string" + }, + "slice_id": { + "nullable": true, + "type": "integer" + }, + "user": { + "$ref": "#/components/schemas/LogRestApi.get.User" + }, + "user_id": { + "nullable": true, + "type": "integer" + } + }, + "type": "object" + }, + "LogRestApi.get.User": { + "properties": { + "username": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "username" + ], + "type": "object" + }, + "LogRestApi.get_list": { + "properties": { + "action": { + "maxLength": 512, + "nullable": true, + "type": "string" + }, + "dashboard_id": { + "nullable": true, + "type": "integer" + }, + "dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "duration_ms": { + "nullable": true, + "type": "integer" + }, + "json": { + "nullable": true, + "type": "string" + }, + "referrer": { + "maxLength": 1024, + "nullable": true, + "type": "string" + }, + "slice_id": { + "nullable": true, + "type": "integer" + }, + "user": { + "$ref": "#/components/schemas/LogRestApi.get_list.User" + }, + "user_id": { + "nullable": true, + "type": "integer" + } + }, + "type": "object" + }, + "LogRestApi.get_list.User": { + "properties": { + "username": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "username" + ], + "type": "object" + }, + "LogRestApi.post": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "LogRestApi.put": { + "properties": { + "action": { + "maxLength": 512, + "nullable": true, + "type": "string" + }, + "dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "json": { + "nullable": true, + "type": "string" + }, + "user": { + "nullable": true + } + }, + "type": "object" + }, + "PermissionApi.get": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 100, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PermissionApi.get_list": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 100, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PermissionApi.post": { + "properties": { + "name": { + "maxLength": 100, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PermissionApi.put": { + "properties": { + "name": { + "maxLength": 100, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PermissionViewMenuApi.get": { + "properties": { + "id": { + "type": "integer" + }, + "permission": { + "$ref": "#/components/schemas/PermissionViewMenuApi.get.Permission" + }, + "view_menu": { + "$ref": "#/components/schemas/PermissionViewMenuApi.get.ViewMenu" + } + }, + "type": "object" + }, + "PermissionViewMenuApi.get.Permission": { + "properties": { + "name": { + "maxLength": 100, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PermissionViewMenuApi.get.ViewMenu": { + "properties": { + "name": { + "maxLength": 250, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PermissionViewMenuApi.get_list": { + "properties": { + "id": { + "type": "integer" + }, + "permission": { + "$ref": "#/components/schemas/PermissionViewMenuApi.get_list.Permission" + }, + "view_menu": { + "$ref": "#/components/schemas/PermissionViewMenuApi.get_list.ViewMenu" + } + }, + "type": "object" + }, + "PermissionViewMenuApi.get_list.Permission": { + "properties": { + "name": { + "maxLength": 100, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PermissionViewMenuApi.get_list.ViewMenu": { + "properties": { + "name": { + "maxLength": 250, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "PermissionViewMenuApi.post": { + "properties": { + "permission_id": { + "nullable": true, + "type": "integer" + }, + "view_menu_id": { + "nullable": true, + "type": "integer" + } + }, + "type": "object" + }, + "PermissionViewMenuApi.put": { + "properties": { + "permission_id": { + "nullable": true, + "type": "integer" + }, + "view_menu_id": { + "nullable": true, + "type": "integer" + } + }, + "type": "object" + }, + "QueryExecutionResponseSchema": { + "properties": { + "columns": { + "items": { + "type": "object" + }, + "type": "array" + }, + "data": { + "items": { + "type": "object" + }, + "type": "array" + }, + "expanded_columns": { + "items": { + "type": "object" + }, + "type": "array" + }, + "query": { + "$ref": "#/components/schemas/QueryResult" + }, + "query_id": { + "type": "integer" + }, + "selected_columns": { + "items": { + "type": "object" + }, + "type": "array" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "QueryRestApi.get": { + "properties": { + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "client_id": { + "maxLength": 11, + "type": "string" + }, + "database": { + "$ref": "#/components/schemas/QueryRestApi.get.Database" + }, + "end_result_backend_time": { + "nullable": true, + "type": "number" + }, + "end_time": { + "nullable": true, + "type": "number" + }, + "error_message": { + "nullable": true, + "type": "string" + }, + "executed_sql": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "limit": { + "nullable": true, + "type": "integer" + }, + "progress": { + "nullable": true, + "type": "integer" + }, + "results_key": { + "maxLength": 64, + "nullable": true, + "type": "string" + }, + "rows": { + "nullable": true, + "type": "integer" + }, + "schema": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "select_as_cta": { + "nullable": true, + "type": "boolean" + }, + "select_as_cta_used": { + "nullable": true, + "type": "boolean" + }, + "select_sql": { + "nullable": true, + "type": "string" + }, + "sql": { + "nullable": true, + "type": "string" + }, + "sql_editor_id": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "start_running_time": { + "nullable": true, + "type": "number" + }, + "start_time": { + "nullable": true, + "type": "number" + }, + "status": { + "maxLength": 16, + "nullable": true, + "type": "string" + }, + "tab_name": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "tmp_schema_name": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "tmp_table_name": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "tracking_url": { + "readOnly": true + } + }, + "required": [ + "client_id", + "database" + ], + "type": "object" + }, + "QueryRestApi.get.Database": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "QueryRestApi.get_list": { + "properties": { + "changed_on": { + "format": "date-time", + "type": "string" + }, + "database": { + "$ref": "#/components/schemas/Database1" + }, + "end_time": { + "type": "number" + }, + "executed_sql": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "rows": { + "type": "integer" + }, + "schema": { + "type": "string" + }, + "sql": { + "type": "string" + }, + "sql_tables": { + "readOnly": true + }, + "start_time": { + "type": "number" + }, + "status": { + "type": "string" + }, + "tab_name": { + "type": "string" + }, + "tmp_table_name": { + "type": "string" + }, + "tracking_url": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/User" + } + }, + "type": "object" + }, + "QueryRestApi.post": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "QueryRestApi.put": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "QueryResult": { + "properties": { + "changed_on": { + "format": "date-time", + "type": "string" + }, + "ctas": { + "type": "boolean" + }, + "db": { + "type": "string" + }, + "dbId": { + "type": "integer" + }, + "endDttm": { + "type": "number" + }, + "errorMessage": { + "nullable": true, + "type": "string" + }, + "executedSql": { + "type": "string" + }, + "extra": { + "type": "object" + }, + "id": { + "type": "string" + }, + "limit": { + "type": "integer" + }, + "limitingFactor": { + "type": "string" + }, + "progress": { + "type": "integer" + }, + "queryId": { + "type": "integer" + }, + "resultsKey": { + "type": "string" + }, + "rows": { + "type": "integer" + }, + "schema": { + "type": "string" + }, + "serverId": { + "type": "integer" + }, + "sql": { + "type": "string" + }, + "sqlEditorId": { + "type": "string" + }, + "startDttm": { + "type": "number" + }, + "state": { + "type": "string" + }, + "tab": { + "type": "string" + }, + "tempSchema": { + "nullable": true, + "type": "string" + }, + "tempTable": { + "nullable": true, + "type": "string" + }, + "trackingUrl": { + "nullable": true, + "type": "string" + }, + "user": { + "type": "string" + }, + "userId": { + "type": "integer" + } + }, + "type": "object" + }, + "RLSRestApi.get": { + "properties": { + "clause": { + "description": "clause_description", + "type": "string" + }, + "description": { + "description": "description_description", + "type": "string" + }, + "filter_type": { + "description": "filter_type_description", + "enum": [ + "Regular", + "Base" + ], + "type": "string" + }, + "group_key": { + "description": "group_key_description", + "type": "string" + }, + "id": { + "description": "id_description", + "type": "integer" + }, + "name": { + "description": "name_description", + "type": "string" + }, + "roles": { + "items": { + "$ref": "#/components/schemas/Roles1" + }, + "type": "array" + }, + "tables": { + "items": { + "$ref": "#/components/schemas/Tables" + }, + "type": "array" + } + }, + "type": "object" + }, + "RLSRestApi.get_list": { + "properties": { + "changed_by": { + "$ref": "#/components/schemas/User" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "clause": { + "description": "clause_description", + "type": "string" + }, + "description": { + "description": "description_description", + "type": "string" + }, + "filter_type": { + "description": "filter_type_description", + "enum": [ + "Regular", + "Base" + ], + "type": "string" + }, + "group_key": { + "description": "group_key_description", + "type": "string" + }, + "id": { + "description": "id_description", + "type": "integer" + }, + "name": { + "description": "name_description", + "type": "string" + }, + "roles": { + "items": { + "$ref": "#/components/schemas/Roles1" + }, + "type": "array" + }, + "tables": { + "items": { + "$ref": "#/components/schemas/Tables" + }, + "type": "array" + } + }, + "type": "object" + }, + "RLSRestApi.post": { + "properties": { + "clause": { + "description": "clause_description", + "type": "string" + }, + "description": { + "description": "description_description", + "nullable": true, + "type": "string" + }, + "filter_type": { + "description": "filter_type_description", + "enum": [ + "Regular", + "Base" + ], + "type": "string" + }, + "group_key": { + "description": "group_key_description", + "nullable": true, + "type": "string" + }, + "name": { + "description": "name_description", + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "roles": { + "description": "roles_description", + "items": { + "type": "integer" + }, + "type": "array" + }, + "tables": { + "description": "tables_description", + "items": { + "type": "integer" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "clause", + "filter_type", + "name", + "roles", + "tables" + ], + "type": "object" + }, + "RLSRestApi.put": { + "properties": { + "clause": { + "description": "clause_description", + "type": "string" + }, + "description": { + "description": "description_description", + "nullable": true, + "type": "string" + }, + "filter_type": { + "description": "filter_type_description", + "enum": [ + "Regular", + "Base" + ], + "type": "string" + }, + "group_key": { + "description": "group_key_description", + "nullable": true, + "type": "string" + }, + "name": { + "description": "name_description", + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "roles": { + "description": "roles_description", + "items": { + "type": "integer" + }, + "type": "array" + }, + "tables": { + "description": "tables_description", + "items": { + "type": "integer" + }, + "type": "array" + } + }, + "type": "object" + }, + "RecentActivity": { + "properties": { + "action": { + "description": "Action taken describing type of activity", + "type": "string" + }, + "item_title": { + "description": "Title of item", + "type": "string" + }, + "item_type": { + "description": "Type of item, e.g. slice or dashboard", + "type": "string" + }, + "item_url": { + "description": "URL to item", + "type": "string" + }, + "time": { + "description": "Time of activity, in epoch milliseconds", + "type": "number" + }, + "time_delta_humanized": { + "description": "Human-readable description of how long ago activity took place.", + "type": "string" + } + }, + "type": "object" + }, + "RecentActivityResponseSchema": { + "properties": { + "result": { + "description": "A list of recent activity objects", + "items": { + "$ref": "#/components/schemas/RecentActivity" + }, + "type": "array" + } + }, + "type": "object" + }, + "RecentActivitySchema": { + "properties": { + "action": { + "description": "Action taken describing type of activity", + "type": "string" + }, + "item_title": { + "description": "Title of item", + "type": "string" + }, + "item_type": { + "description": "Type of item, e.g. slice or dashboard", + "type": "string" + }, + "item_url": { + "description": "URL to item", + "type": "string" + }, + "time": { + "description": "Time of activity, in epoch milliseconds", + "type": "number" + }, + "time_delta_humanized": { + "description": "Human-readable description of how long ago activity took place.", + "type": "string" + } + }, + "type": "object" + }, + "RelatedResponseSchema": { + "properties": { + "count": { + "description": "The total number of related values", + "type": "integer" + }, + "result": { + "items": { + "$ref": "#/components/schemas/RelatedResultResponse" + }, + "type": "array" + } + }, + "type": "object" + }, + "RelatedResultResponse": { + "properties": { + "extra": { + "description": "The extra metadata for related item", + "type": "object" + }, + "text": { + "description": "The related item string representation", + "type": "string" + }, + "value": { + "description": "The related item identifier", + "type": "integer" + } + }, + "type": "object" + }, + "ReportExecutionLogRestApi.get": { + "properties": { + "end_dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "error_message": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "scheduled_dttm": { + "format": "date-time", + "type": "string" + }, + "start_dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "state": { + "maxLength": 50, + "type": "string" + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + }, + "value": { + "nullable": true, + "type": "number" + }, + "value_row_json": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "scheduled_dttm", + "state" + ], + "type": "object" + }, + "ReportExecutionLogRestApi.get_list": { + "properties": { + "end_dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "error_message": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "scheduled_dttm": { + "format": "date-time", + "type": "string" + }, + "start_dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "state": { + "maxLength": 50, + "type": "string" + }, + "uuid": { + "format": "uuid", + "nullable": true, + "type": "string" + }, + "value": { + "nullable": true, + "type": "number" + }, + "value_row_json": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "scheduled_dttm", + "state" + ], + "type": "object" + }, + "ReportExecutionLogRestApi.post": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "ReportExecutionLogRestApi.put": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "ReportRecipient": { + "properties": { + "recipient_config_json": { + "$ref": "#/components/schemas/ReportRecipientConfigJSON" + }, + "type": { + "description": "The recipient type, check spec for valid options", + "enum": [ + "Email", + "Slack", + "SlackV2" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "ReportRecipientConfigJSON": { + "properties": { + "bccTarget": { + "type": "string" + }, + "ccTarget": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "type": "object" + }, + "ReportScheduleRestApi.get": { + "properties": { + "active": { + "nullable": true, + "type": "boolean" + }, + "chart": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get.Slice" + }, + "context_markdown": { + "nullable": true, + "type": "string" + }, + "creation_method": { + "maxLength": 255, + "nullable": true, + "type": "string" + }, + "crontab": { + "maxLength": 1000, + "type": "string" + }, + "custom_width": { + "nullable": true, + "type": "integer" + }, + "dashboard": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get.Dashboard" + }, + "database": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get.Database" + }, + "description": { + "nullable": true, + "type": "string" + }, + "email_subject": { + "maxLength": 255, + "nullable": true, + "type": "string" + }, + "extra": { + "readOnly": true + }, + "force_screenshot": { + "nullable": true, + "type": "boolean" + }, + "grace_period": { + "nullable": true, + "type": "integer" + }, + "id": { + "type": "integer" + }, + "last_eval_dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "last_state": { + "maxLength": 50, + "nullable": true, + "type": "string" + }, + "last_value": { + "nullable": true, + "type": "number" + }, + "last_value_row_json": { + "nullable": true, + "type": "string" + }, + "log_retention": { + "nullable": true, + "type": "integer" + }, + "name": { + "maxLength": 150, + "type": "string" + }, + "owners": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get.User" + }, + "recipients": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get.ReportRecipients" + }, + "report_format": { + "maxLength": 50, + "nullable": true, + "type": "string" + }, + "sql": { + "nullable": true, + "type": "string" + }, + "timezone": { + "maxLength": 100, + "type": "string" + }, + "type": { + "maxLength": 50, + "type": "string" + }, + "validator_config_json": { + "nullable": true, + "type": "string" + }, + "validator_type": { + "maxLength": 100, + "nullable": true, + "type": "string" + }, + "working_timeout": { + "nullable": true, + "type": "integer" + } + }, + "required": [ + "crontab", + "name", + "recipients", + "type" + ], + "type": "object" + }, + "ReportScheduleRestApi.get.Dashboard": { + "properties": { + "dashboard_title": { + "maxLength": 500, + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "ReportScheduleRestApi.get.Database": { + "properties": { + "database_name": { + "maxLength": 250, + "type": "string" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "database_name" + ], + "type": "object" + }, + "ReportScheduleRestApi.get.ReportRecipients": { + "properties": { + "id": { + "type": "integer" + }, + "recipient_config_json": { + "nullable": true, + "type": "string" + }, + "type": { + "maxLength": 50, + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "ReportScheduleRestApi.get.Slice": { + "properties": { + "id": { + "type": "integer" + }, + "slice_name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "viz_type": { + "maxLength": 250, + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "ReportScheduleRestApi.get.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ReportScheduleRestApi.get_list": { + "properties": { + "active": { + "nullable": true, + "type": "boolean" + }, + "changed_by": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get_list.User" + }, + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "chart_id": { + "nullable": true, + "type": "integer" + }, + "created_by": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get_list.User1" + }, + "created_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "creation_method": { + "maxLength": 255, + "nullable": true, + "type": "string" + }, + "crontab": { + "maxLength": 1000, + "type": "string" + }, + "crontab_humanized": { + "readOnly": true + }, + "dashboard_id": { + "nullable": true, + "type": "integer" + }, + "description": { + "nullable": true, + "type": "string" + }, + "extra": { + "readOnly": true + }, + "id": { + "type": "integer" + }, + "last_eval_dttm": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "last_state": { + "maxLength": 50, + "nullable": true, + "type": "string" + }, + "name": { + "maxLength": 150, + "type": "string" + }, + "owners": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get_list.User2" + }, + "recipients": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get_list.ReportRecipients" + }, + "timezone": { + "maxLength": 100, + "type": "string" + }, + "type": { + "maxLength": 50, + "type": "string" + } + }, + "required": [ + "crontab", + "name", + "recipients", + "type" + ], + "type": "object" + }, + "ReportScheduleRestApi.get_list.ReportRecipients": { + "properties": { + "id": { + "type": "integer" + }, + "type": { + "maxLength": 50, + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "ReportScheduleRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ReportScheduleRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ReportScheduleRestApi.get_list.User2": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "ReportScheduleRestApi.post": { + "properties": { + "active": { + "type": "boolean" + }, + "chart": { + "nullable": true, + "type": "integer" + }, + "context_markdown": { + "description": "Markdown description", + "nullable": true, + "type": "string" + }, + "creation_method": { + "description": "Creation method is used to inform the frontend whether the report/alert was created in the dashboard, chart, or alerts and reports UI.", + "enum": [ + "charts", + "dashboards", + "alerts_reports" + ] + }, + "crontab": { + "description": "A CRON expression.[Crontab Guru](https://crontab.guru/) is a helpful resource that can help you craft a CRON expression.", + "example": "*/5 * * * *", + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + "custom_width": { + "description": "Custom width of the screenshot in pixels", + "example": 1000, + "nullable": true, + "type": "integer" + }, + "dashboard": { + "nullable": true, + "type": "integer" + }, + "database": { + "type": "integer" + }, + "description": { + "description": "Use a nice description to give context to this Alert/Report", + "example": "Daily sales dashboard to marketing", + "nullable": true, + "type": "string" + }, + "email_subject": { + "description": "The report schedule subject line", + "example": "[Report] Report name: Dashboard or chart name", + "nullable": true, + "type": "string" + }, + "extra": { + "type": "object" + }, + "force_screenshot": { + "type": "boolean" + }, + "grace_period": { + "description": "Once an alert is triggered, how long, in seconds, before Superset nags you again. (in seconds)", + "example": 14400, + "minimum": 1, + "type": "integer" + }, + "log_retention": { + "description": "How long to keep the logs around for this report (in days)", + "example": 90, + "minimum": 1, + "type": "integer" + }, + "name": { + "description": "The report schedule name.", + "example": "Daily dashboard email", + "maxLength": 150, + "minLength": 1, + "type": "string" + }, + "owners": { + "items": { + "description": "Owner are users ids allowed to delete or change this report. If left empty you will be one of the owners of the report.", + "type": "integer" + }, + "type": "array" + }, + "recipients": { + "items": { + "$ref": "#/components/schemas/ReportRecipient" + }, + "type": "array" + }, + "report_format": { + "enum": [ + "PDF", + "PNG", + "CSV", + "TEXT" + ], + "type": "string" + }, + "selected_tabs": { + "items": { + "type": "integer" + }, + "nullable": true, + "type": "array" + }, + "sql": { + "description": "A SQL statement that defines whether the alert should get triggered or not. The query is expected to return either NULL or a number value.", + "example": "SELECT value FROM time_series_table", + "type": "string" + }, + "timezone": { + "description": "A timezone string that represents the location of the timezone.", + "enum": [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Kyiv", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu" + ], + "type": "string" + }, + "type": { + "description": "The report schedule type", + "enum": [ + "Alert", + "Report" + ], + "type": "string" + }, + "validator_config_json": { + "$ref": "#/components/schemas/ValidatorConfigJSON" + }, + "validator_type": { + "description": "Determines when to trigger alert based off value from alert query. Alerts will be triggered with these validator types:\n- Not Null - When the return value is Not NULL, Empty, or 0\n- Operator - When `sql_return_value comparison_operator threshold` is True e.g. `50 <= 75`
Supports the comparison operators <, <=, >, >=, ==, and !=", + "enum": [ + "not null", + "operator" + ], + "type": "string" + }, + "working_timeout": { + "description": "If an alert is staled at a working state, how long until it's state is reset to error", + "example": 3600, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "crontab", + "name", + "type" + ], + "type": "object" + }, + "ReportScheduleRestApi.put": { + "properties": { + "active": { + "type": "boolean" + }, + "chart": { + "nullable": true, + "type": "integer" + }, + "context_markdown": { + "description": "Markdown description", + "nullable": true, + "type": "string" + }, + "creation_method": { + "description": "Creation method is used to inform the frontend whether the report/alert was created in the dashboard, chart, or alerts and reports UI.", + "enum": [ + "charts", + "dashboards", + "alerts_reports" + ], + "nullable": true + }, + "crontab": { + "description": "A CRON expression.[Crontab Guru](https://crontab.guru/) is a helpful resource that can help you craft a CRON expression.", + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + "custom_width": { + "description": "Custom width of the screenshot in pixels", + "example": 1000, + "nullable": true, + "type": "integer" + }, + "dashboard": { + "nullable": true, + "type": "integer" + }, + "database": { + "type": "integer" + }, + "description": { + "description": "Use a nice description to give context to this Alert/Report", + "example": "Daily sales dashboard to marketing", + "nullable": true, + "type": "string" + }, + "email_subject": { + "description": "The report schedule subject line", + "example": "[Report] Report name: Dashboard or chart name", + "nullable": true, + "type": "string" + }, + "extra": { + "type": "object" + }, + "force_screenshot": { + "type": "boolean" + }, + "grace_period": { + "description": "Once an alert is triggered, how long, in seconds, before Superset nags you again. (in seconds)", + "example": 14400, + "minimum": 1, + "type": "integer" + }, + "log_retention": { + "description": "How long to keep the logs around for this report (in days)", + "example": 90, + "minimum": 0, + "type": "integer" + }, + "name": { + "description": "The report schedule name.", + "maxLength": 150, + "minLength": 1, + "type": "string" + }, + "owners": { + "items": { + "description": "Owner are users ids allowed to delete or change this report. If left empty you will be one of the owners of the report.", + "type": "integer" + }, + "type": "array" + }, + "recipients": { + "items": { + "$ref": "#/components/schemas/ReportRecipient" + }, + "type": "array" + }, + "report_format": { + "enum": [ + "PDF", + "PNG", + "CSV", + "TEXT" + ], + "type": "string" + }, + "sql": { + "description": "A SQL statement that defines whether the alert should get triggered or not. The query is expected to return either NULL or a number value.", + "example": "SELECT value FROM time_series_table", + "nullable": true, + "type": "string" + }, + "timezone": { + "description": "A timezone string that represents the location of the timezone.", + "enum": [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Kyiv", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu" + ], + "type": "string" + }, + "type": { + "description": "The report schedule type", + "enum": [ + "Alert", + "Report" + ], + "type": "string" + }, + "validator_config_json": { + "$ref": "#/components/schemas/ValidatorConfigJSON" + }, + "validator_type": { + "description": "Determines when to trigger alert based off value from alert query. Alerts will be triggered with these validator types:\n- Not Null - When the return value is Not NULL, Empty, or 0\n- Operator - When `sql_return_value comparison_operator threshold` is True e.g. `50 <= 75`
Supports the comparison operators <, <=, >, >=, ==, and !=", + "enum": [ + "not null", + "operator" + ], + "nullable": true, + "type": "string" + }, + "working_timeout": { + "description": "If an alert is staled at a working state, how long until it's state is reset to error", + "example": 3600, + "minimum": 1, + "nullable": true, + "type": "integer" + } + }, + "type": "object" + }, + "Resource": { + "properties": { + "id": { + "type": "string" + }, + "type": { + "enum": [ + "dashboard" + ] + } + }, + "required": [ + "id", + "type" + ], + "type": "object" + }, + "RlsRule": { + "properties": { + "clause": { + "type": "string" + }, + "dataset": { + "type": "integer" + } + }, + "required": [ + "clause" + ], + "type": "object" + }, + "RolePermissionListSchema": { + "properties": { + "id": { + "type": "integer" + }, + "permission_name": { + "type": "string" + }, + "view_menu_name": { + "type": "string" + } + }, + "type": "object" + }, + "RolePermissionPostSchema": { + "properties": { + "permission_view_menu_ids": { + "description": "List of permission view menu id", + "items": { + "type": "integer" + }, + "type": "array" + } + }, + "required": [ + "permission_view_menu_ids" + ], + "type": "object" + }, + "RoleResponseSchema": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "permission_ids": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "user_ids": { + "items": { + "type": "integer" + }, + "type": "array" + } + }, + "type": "object" + }, + "RoleUserPutSchema": { + "properties": { + "user_ids": { + "description": "List of user ids", + "items": { + "type": "integer" + }, + "type": "array" + } + }, + "required": [ + "user_ids" + ], + "type": "object" + }, + "Roles": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "Roles1": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "RolesResponseSchema": { + "properties": { + "count": { + "type": "integer" + }, + "ids": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "result": { + "items": { + "$ref": "#/components/schemas/RoleResponseSchema" + }, + "type": "array" + } + }, + "type": "object" + }, + "SQLLabBootstrapSchema": { + "properties": { + "active_tab": { + "$ref": "#/components/schemas/TabState" + }, + "databases": { + "additionalProperties": { + "$ref": "#/components/schemas/ImportV1Database" + }, + "type": "object" + }, + "queries": { + "additionalProperties": { + "$ref": "#/components/schemas/QueryResult" + }, + "type": "object" + }, + "tab_state_ids": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "SavedQueryRestApi.get": { + "properties": { + "catalog": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "changed_by": { + "$ref": "#/components/schemas/SavedQueryRestApi.get.User" + }, + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/SavedQueryRestApi.get.User1" + }, + "database": { + "$ref": "#/components/schemas/SavedQueryRestApi.get.Database" + }, + "description": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "label": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "schema": { + "maxLength": 128, + "nullable": true, + "type": "string" + }, + "sql": { + "nullable": true, + "type": "string" + }, + "sql_tables": { + "readOnly": true + }, + "template_parameters": { + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "SavedQueryRestApi.get.Database": { + "properties": { + "database_name": { + "maxLength": 250, + "type": "string" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "database_name" + ], + "type": "object" + }, + "SavedQueryRestApi.get.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "SavedQueryRestApi.get.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "SavedQueryRestApi.get_list": { + "properties": { + "catalog": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "changed_by": { + "$ref": "#/components/schemas/SavedQueryRestApi.get_list.User" + }, + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/SavedQueryRestApi.get_list.User1" + }, + "created_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "database": { + "$ref": "#/components/schemas/SavedQueryRestApi.get_list.Database" + }, + "db_id": { + "nullable": true, + "type": "integer" + }, + "description": { + "nullable": true, + "type": "string" + }, + "extra": { + "readOnly": true + }, + "id": { + "type": "integer" + }, + "label": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "last_run_delta_humanized": { + "readOnly": true + }, + "rows": { + "nullable": true, + "type": "integer" + }, + "schema": { + "maxLength": 128, + "nullable": true, + "type": "string" + }, + "sql": { + "nullable": true, + "type": "string" + }, + "sql_tables": { + "readOnly": true + }, + "tags": { + "$ref": "#/components/schemas/SavedQueryRestApi.get_list.Tag" + } + }, + "type": "object" + }, + "SavedQueryRestApi.get_list.Database": { + "properties": { + "database_name": { + "maxLength": 250, + "type": "string" + }, + "id": { + "type": "integer" + } + }, + "required": [ + "database_name" + ], + "type": "object" + }, + "SavedQueryRestApi.get_list.Tag": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "type": { + "enum": [ + 1, + 2, + 3, + 4 + ] + } + }, + "type": "object" + }, + "SavedQueryRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "SavedQueryRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "SavedQueryRestApi.post": { + "properties": { + "catalog": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "db_id": { + "nullable": true, + "type": "integer" + }, + "description": { + "nullable": true, + "type": "string" + }, + "extra_json": { + "nullable": true, + "type": "string" + }, + "label": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "schema": { + "maxLength": 128, + "nullable": true, + "type": "string" + }, + "sql": { + "nullable": true, + "type": "string" + }, + "template_parameters": { + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "SavedQueryRestApi.put": { + "properties": { + "catalog": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "db_id": { + "nullable": true, + "type": "integer" + }, + "description": { + "nullable": true, + "type": "string" + }, + "extra_json": { + "nullable": true, + "type": "string" + }, + "label": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "schema": { + "maxLength": 128, + "nullable": true, + "type": "string" + }, + "sql": { + "nullable": true, + "type": "string" + }, + "template_parameters": { + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "SchemasResponseSchema": { + "properties": { + "result": { + "items": { + "description": "A database schema name", + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "SelectStarResponseSchema": { + "properties": { + "result": { + "description": "SQL select star", + "type": "string" + } + }, + "type": "object" + }, + "Slice": { + "properties": { + "cache_timeout": { + "description": "Duration (in seconds) of the caching timeout for this chart.", + "type": "integer" + }, + "certification_details": { + "description": "Details of the certification.", + "type": "string" + }, + "certified_by": { + "description": "Person or group that has certified this dashboard.", + "type": "string" + }, + "changed_on": { + "description": "Timestamp of the last modification.", + "format": "date-time", + "type": "string" + }, + "changed_on_humanized": { + "description": "Timestamp of the last modification in human readable form.", + "type": "string" + }, + "datasource": { + "description": "Datasource identifier.", + "type": "string" + }, + "description": { + "description": "Slice description.", + "type": "string" + }, + "description_markeddown": { + "description": "Sanitized HTML version of the chart description.", + "type": "string" + }, + "edit_url": { + "description": "The URL for editing the slice.", + "type": "string" + }, + "form_data": { + "description": "Form data associated with the slice.", + "type": "object" + }, + "is_managed_externally": { + "description": "If the chart is managed outside externally.", + "type": "boolean" + }, + "modified": { + "description": "Last modification in human readable form.", + "type": "string" + }, + "owners": { + "description": "Owners identifiers.", + "items": { + "type": "integer" + }, + "type": "array" + }, + "query_context": { + "description": "The context associated with the query.", + "type": "object" + }, + "slice_id": { + "description": "The slice ID.", + "type": "integer" + }, + "slice_name": { + "description": "The slice name.", + "type": "string" + }, + "slice_url": { + "description": "The slice URL.", + "type": "string" + } + }, + "type": "object" + }, + "SqlLabPermalinkSchema": { + "properties": { + "autorun": { + "type": "boolean" + }, + "catalog": { + "description": "The catalog name of the query", + "nullable": true, + "type": "string" + }, + "dbId": { + "description": "The id of the database", + "type": "integer" + }, + "name": { + "description": "The label of the editor tab", + "type": "string" + }, + "schema": { + "description": "The schema name of the query", + "nullable": true, + "type": "string" + }, + "sql": { + "description": "SQL query text", + "type": "string" + }, + "templateParams": { + "description": "stringfied JSON string for template parameters", + "nullable": true, + "type": "string" + } + }, + "required": [ + "dbId", + "name", + "sql" + ], + "type": "object" + }, + "StopQuerySchema": { + "properties": { + "client_id": { + "type": "string" + } + }, + "type": "object" + }, + "SupersetRoleApi.get": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SupersetRoleApi.get_list": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SupersetRoleApi.post": { + "properties": { + "name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SupersetRoleApi.put": { + "properties": { + "name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SupersetUserApi.get": { + "properties": { + "active": { + "nullable": true, + "type": "boolean" + }, + "changed_by": { + "$ref": "#/components/schemas/SupersetUserApi.get.User1" + }, + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "created_by": { + "$ref": "#/components/schemas/SupersetUserApi.get.User" + }, + "created_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "email": { + "maxLength": 320, + "type": "string" + }, + "fail_login_count": { + "nullable": true, + "type": "integer" + }, + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + }, + "login_count": { + "nullable": true, + "type": "integer" + }, + "roles": { + "$ref": "#/components/schemas/SupersetUserApi.get.Role" + }, + "username": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "email", + "first_name", + "last_name", + "username" + ], + "type": "object" + }, + "SupersetUserApi.get.Role": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SupersetUserApi.get.User": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "SupersetUserApi.get.User1": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "SupersetUserApi.get_list": { + "properties": { + "active": { + "nullable": true, + "type": "boolean" + }, + "changed_by": { + "$ref": "#/components/schemas/SupersetUserApi.get_list.User1" + }, + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "created_by": { + "$ref": "#/components/schemas/SupersetUserApi.get_list.User" + }, + "created_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "email": { + "maxLength": 320, + "type": "string" + }, + "fail_login_count": { + "nullable": true, + "type": "integer" + }, + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + }, + "login_count": { + "nullable": true, + "type": "integer" + }, + "roles": { + "$ref": "#/components/schemas/SupersetUserApi.get_list.Role" + }, + "username": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "email", + "first_name", + "last_name", + "username" + ], + "type": "object" + }, + "SupersetUserApi.get_list.Role": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SupersetUserApi.get_list.User": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "SupersetUserApi.get_list.User1": { + "properties": { + "id": { + "type": "integer" + } + }, + "type": "object" + }, + "SupersetUserApi.post": { + "properties": { + "active": { + "description": "Is user active?It's not a good policy to remove a user, just make it inactive", + "type": "boolean" + }, + "email": { + "description": "The user's email", + "type": "string" + }, + "first_name": { + "description": "The user's first name", + "type": "string" + }, + "last_name": { + "description": "The user's last name", + "type": "string" + }, + "password": { + "description": "The user's password for authentication", + "type": "string" + }, + "roles": { + "description": "The user's roles", + "items": { + "type": "integer" + }, + "minItems": 1, + "type": "array" + }, + "username": { + "description": "The user's username", + "maxLength": 250, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "email", + "first_name", + "last_name", + "password", + "roles", + "username" + ], + "type": "object" + }, + "SupersetUserApi.put": { + "properties": { + "active": { + "description": "Is user active?It's not a good policy to remove a user, just make it inactive", + "type": "boolean" + }, + "email": { + "description": "The user's email", + "type": "string" + }, + "first_name": { + "description": "The user's first name", + "type": "string" + }, + "last_name": { + "description": "The user's last name", + "type": "string" + }, + "password": { + "description": "The user's password for authentication", + "type": "string" + }, + "roles": { + "description": "The user's roles", + "items": { + "type": "integer" + }, + "minItems": 1, + "type": "array" + }, + "username": { + "description": "The user's username", + "maxLength": 250, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "Tab": { + "properties": { + "children": { + "items": { + "$ref": "#/components/schemas/Tab" + }, + "type": "array" + }, + "parents": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + "TabState": { + "properties": { + "active": { + "type": "boolean" + }, + "autorun": { + "type": "boolean" + }, + "database_id": { + "type": "integer" + }, + "extra_json": { + "type": "object" + }, + "hide_left_bar": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "latest_query": { + "$ref": "#/components/schemas/QueryResult" + }, + "query_limit": { + "type": "integer" + }, + "saved_query": { + "nullable": true, + "type": "object" + }, + "schema": { + "type": "string" + }, + "sql": { + "type": "string" + }, + "table_schemas": { + "items": { + "$ref": "#/components/schemas/Table" + }, + "type": "array" + }, + "user_id": { + "type": "integer" + } + }, + "type": "object" + }, + "Table": { + "properties": { + "database_id": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "expanded": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "schema": { + "type": "string" + }, + "tab_state_id": { + "type": "integer" + }, + "table": { + "type": "string" + } + }, + "type": "object" + }, + "TableExtraMetadataResponseSchema": { + "properties": { + "clustering": { + "type": "object" + }, + "metadata": { + "type": "object" + }, + "partitions": { + "type": "object" + } + }, + "type": "object" + }, + "TableMetadataColumnsResponse": { + "properties": { + "duplicates_constraint": { + "type": "string" + }, + "keys": { + "description": "", + "items": { + "type": "string" + }, + "type": "array" + }, + "longType": { + "description": "The actual backend long type for the column", + "type": "string" + }, + "name": { + "description": "The column name", + "type": "string" + }, + "type": { + "description": "The column type", + "type": "string" + } + }, + "type": "object" + }, + "TableMetadataForeignKeysIndexesResponse": { + "properties": { + "column_names": { + "items": { + "description": "A list of column names that compose the foreign key or index", + "type": "string" + }, + "type": "array" + }, + "name": { + "description": "The name of the foreign key or index", + "type": "string" + }, + "options": { + "$ref": "#/components/schemas/TableMetadataOptionsResponse" + }, + "referred_columns": { + "items": { + "type": "string" + }, + "type": "array" + }, + "referred_schema": { + "type": "string" + }, + "referred_table": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "TableMetadataOptionsResponse": { + "properties": { + "deferrable": { + "type": "boolean" + }, + "initially": { + "type": "boolean" + }, + "match": { + "type": "boolean" + }, + "ondelete": { + "type": "boolean" + }, + "onupdate": { + "type": "boolean" + } + }, + "type": "object" + }, + "TableMetadataPrimaryKeyResponse": { + "properties": { + "column_names": { + "items": { + "description": "A list of column names that compose the primary key", + "type": "string" + }, + "type": "array" + }, + "name": { + "description": "The primary key index name", + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "TableMetadataResponseSchema": { + "properties": { + "columns": { + "description": "A list of columns and their metadata", + "items": { + "$ref": "#/components/schemas/TableMetadataColumnsResponse" + }, + "type": "array" + }, + "foreignKeys": { + "description": "A list of foreign keys and their metadata", + "items": { + "$ref": "#/components/schemas/TableMetadataForeignKeysIndexesResponse" + }, + "type": "array" + }, + "indexes": { + "description": "A list of indexes and their metadata", + "items": { + "$ref": "#/components/schemas/TableMetadataForeignKeysIndexesResponse" + }, + "type": "array" + }, + "name": { + "description": "The name of the table", + "type": "string" + }, + "primaryKey": { + "allOf": [ + { + "$ref": "#/components/schemas/TableMetadataPrimaryKeyResponse" + } + ], + "description": "Primary keys metadata" + }, + "selectStar": { + "description": "SQL select star", + "type": "string" + } + }, + "type": "object" + }, + "Tables": { + "properties": { + "id": { + "type": "integer" + }, + "schema": { + "type": "string" + }, + "table_name": { + "type": "string" + } + }, + "type": "object" + }, + "TabsPayloadSchema": { + "properties": { + "all_tabs": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "tab_tree": { + "items": { + "$ref": "#/components/schemas/Tab" + }, + "type": "array" + } + }, + "type": "object" + }, + "Tag": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "enum": [ + 1, + 2, + 3, + 4 + ] + } + }, + "type": "object" + }, + "TagGetResponseSchema": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "TagObject": { + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "objects_to_tag": { + "description": "Objects to tag", + "items": {}, + "type": "array" + } + }, + "type": "object" + }, + "TagPostBulkResponseObject": { + "properties": { + "objects_skipped": { + "description": "Objects to tag", + "items": {}, + "type": "array" + }, + "objects_tagged": { + "description": "Objects to tag", + "items": {}, + "type": "array" + } + }, + "type": "object" + }, + "TagPostBulkResponseSchema": { + "properties": { + "result": { + "$ref": "#/components/schemas/TagPostBulkResponseObject" + } + }, + "type": "object" + }, + "TagPostBulkSchema": { + "properties": { + "tags": { + "items": { + "$ref": "#/components/schemas/TagObject" + }, + "type": "array" + } + }, + "type": "object" + }, + "TagRestApi.get": { + "properties": { + "changed_by": { + "$ref": "#/components/schemas/TagRestApi.get.User" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/TagRestApi.get.User1" + }, + "created_on_delta_humanized": { + "readOnly": true + }, + "description": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "type": { + "enum": [ + 1, + 2, + 3, + 4 + ] + } + }, + "type": "object" + }, + "TagRestApi.get.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "TagRestApi.get.User1": { + "properties": { + "active": { + "nullable": true, + "type": "boolean" + }, + "changed_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "created_on": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "email": { + "maxLength": 320, + "type": "string" + }, + "fail_login_count": { + "nullable": true, + "type": "integer" + }, + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + }, + "login_count": { + "nullable": true, + "type": "integer" + }, + "password": { + "maxLength": 256, + "nullable": true, + "type": "string" + }, + "username": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "email", + "first_name", + "last_name", + "username" + ], + "type": "object" + }, + "TagRestApi.get_list": { + "properties": { + "changed_by": { + "$ref": "#/components/schemas/TagRestApi.get_list.User" + }, + "changed_on_delta_humanized": { + "readOnly": true + }, + "created_by": { + "$ref": "#/components/schemas/TagRestApi.get_list.User1" + }, + "created_on_delta_humanized": { + "readOnly": true + }, + "description": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "nullable": true, + "type": "string" + }, + "type": { + "enum": [ + 1, + 2, + 3, + 4 + ] + } + }, + "type": "object" + }, + "TagRestApi.get_list.User": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "TagRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "TagRestApi.post": { + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "objects_to_tag": { + "description": "Objects to tag", + "items": {}, + "type": "array" + } + }, + "type": "object" + }, + "TagRestApi.put": { + "properties": { + "description": { + "nullable": true, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "objects_to_tag": { + "description": "Objects to tag", + "items": {}, + "type": "array" + } + }, + "type": "object" + }, + "TaggedObjectEntityResponseSchema": { + "properties": { + "changed_on": { + "format": "date-time", + "type": "string" + }, + "created_by": { + "$ref": "#/components/schemas/User" + }, + "creator": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "owners": { + "items": { + "$ref": "#/components/schemas/User1" + }, + "type": "array" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/TagGetResponseSchema" + }, + "type": "array" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "TemporaryCachePostSchema": { + "properties": { + "value": { + "description": "Any type of JSON supported text.", + "type": "string" + } + }, + "required": [ + "value" + ], + "type": "object" + }, + "TemporaryCachePutSchema": { + "properties": { + "value": { + "description": "Any type of JSON supported text.", + "type": "string" + } + }, + "required": [ + "value" + ], + "type": "object" + }, + "UploadFileMetadata": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/UploadFileMetadataItem" + }, + "type": "array" + } + }, + "type": "object" + }, + "UploadFileMetadataItem": { + "properties": { + "column_names": { + "description": "A list of columns names in the sheet", + "items": { + "type": "string" + }, + "type": "array" + }, + "sheet_name": { + "description": "The name of the sheet", + "type": "string" + } + }, + "type": "object" + }, + "UploadFileMetadataPostSchema": { + "properties": { + "delimiter": { + "description": "The character used to separate values in the CSV file (e.g., a comma, semicolon, or tab).", + "type": "string" + }, + "file": { + "description": "The file to upload", + "format": "binary", + "type": "string" + }, + "header_row": { + "description": "Row containing the headers to use as column names(0 is first line of data). Leave empty if there is no header row.", + "type": "integer" + }, + "type": { + "description": "File type to upload", + "enum": [ + "csv", + "excel", + "columnar" + ] + } + }, + "required": [ + "file", + "type" + ], + "type": "object" + }, + "UploadPostSchema": { + "properties": { + "already_exists": { + "default": "fail", + "description": "What to do if the table already exists accepts: fail, replace, append", + "enum": [ + "fail", + "replace", + "append" + ], + "type": "string" + }, + "column_data_types": { + "description": "[CSV only] A dictionary with column names and their data types if you need to change the defaults. Example: {'user_id':'int'}. Check Python Pandas library for supported data types", + "type": "string" + }, + "column_dates": { + "description": "[CSV and Excel only] A list of column names that should be parsed as dates. Example: date,timestamp", + "items": { + "type": "string" + }, + "type": "array" + }, + "columns_read": { + "description": "A List of the column names that should be read", + "items": { + "type": "string" + }, + "type": "array" + }, + "dataframe_index": { + "description": "Write dataframe index as a column.", + "type": "boolean" + }, + "day_first": { + "description": "[CSV only] DD/MM format dates, international and European format", + "type": "boolean" + }, + "decimal_character": { + "description": "[CSV and Excel only] Character to recognize as decimal point. Default is '.'", + "type": "string" + }, + "delimiter": { + "description": "[CSV only] The character used to separate values in the CSV file (e.g., a comma, semicolon, or tab).", + "type": "string" + }, + "file": { + "description": "The file to upload", + "format": "text/csv", + "type": "string" + }, + "header_row": { + "description": "[CSV and Excel only] Row containing the headers to use as column names (0 is first line of data). Leave empty if there is no header row.", + "type": "integer" + }, + "index_column": { + "description": "[CSV and Excel only] Column to use as the row labels of the dataframe. Leave empty if no index column", + "type": "string" + }, + "index_label": { + "description": "Index label for index column.", + "type": "string" + }, + "null_values": { + "description": "[CSV and Excel only] A list of strings that should be treated as null. Examples: '' for empty strings, 'None', 'N/A', Warning: Hive database supports only a single value", + "items": { + "type": "string" + }, + "type": "array" + }, + "rows_to_read": { + "description": "[CSV and Excel only] Number of rows to read from the file. If None, reads all rows.", + "minimum": 1, + "nullable": true, + "type": "integer" + }, + "schema": { + "description": "The schema to upload the data file to.", + "type": "string" + }, + "sheet_name": { + "description": "[Excel only]] Strings used for sheet names (default is the first sheet).", + "type": "string" + }, + "skip_blank_lines": { + "description": "[CSV only] Skip blank lines in the CSV file.", + "type": "boolean" + }, + "skip_initial_space": { + "description": "[CSV only] Skip spaces after delimiter.", + "type": "boolean" + }, + "skip_rows": { + "description": "[CSV and Excel only] Number of rows to skip at start of file.", + "type": "integer" + }, + "table_name": { + "description": "The name of the table to be created/appended", + "maxLength": 10000, + "minLength": 1, + "type": "string" + }, + "type": { + "description": "File type to upload", + "enum": [ + "csv", + "excel", + "columnar" + ] + } + }, + "required": [ + "file", + "table_name", + "type" + ], + "type": "object" + }, + "User": { + "properties": { + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "type": "string" + } + }, + "type": "object" + }, + "User1": { + "properties": { + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "User2": { + "properties": { + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "UserResponseSchema": { + "properties": { + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "is_anonymous": { + "type": "boolean" + }, + "last_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "ValidateSQLRequest": { + "properties": { + "catalog": { + "nullable": true, + "type": "string" + }, + "schema": { + "nullable": true, + "type": "string" + }, + "sql": { + "description": "SQL statement to validate", + "type": "string" + }, + "template_params": { + "nullable": true, + "type": "object" + } + }, + "required": [ + "sql" + ], + "type": "object" + }, + "ValidateSQLResponse": { + "properties": { + "end_column": { + "type": "integer" + }, + "line_number": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "start_column": { + "type": "integer" + } + }, + "type": "object" + }, + "ValidatorConfigJSON": { + "properties": { + "op": { + "description": "The operation to compare with a threshold to apply to the SQL output\n", + "enum": [ + "<", + "<=", + ">", + ">=", + "==", + "!=" + ], + "type": "string" + }, + "threshold": { + "type": "number" + } + }, + "type": "object" + }, + "ViewMenuApi.get": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ViewMenuApi.get_list": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "maxLength": 250, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ViewMenuApi.post": { + "properties": { + "name": { + "maxLength": 250, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ViewMenuApi.put": { + "properties": { + "name": { + "maxLength": 250, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "advanced_data_type_convert_schema": { + "properties": { + "type": { + "default": "port", + "type": "string" + }, + "values": { + "items": { + "default": "http" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "type", + "values" + ], + "type": "object" + }, + "database_catalogs_query_schema": { + "properties": { + "force": { + "type": "boolean" + } + }, + "type": "object" + }, + "database_schemas_query_schema": { + "properties": { + "catalog": { + "type": "string" + }, + "force": { + "type": "boolean" + }, + "upload_allowed": { + "type": "boolean" + } + }, + "type": "object" + }, + "database_tables_query_schema": { + "properties": { + "catalog_name": { + "type": "string" + }, + "force": { + "type": "boolean" + }, + "schema_name": { + "type": "string" + } + }, + "required": [ + "schema_name" + ], + "type": "object" + }, + "delete_tags_schema": { + "items": { + "type": "string" + }, + "type": "array" + }, + "get_delete_ids_schema": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "get_export_ids_schema": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "get_fav_star_ids_schema": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "get_info_schema": { + "properties": { + "add_columns": { + "additionalProperties": { + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + } + }, + "type": "object" + }, + "type": "object" + }, + "edit_columns": { + "additionalProperties": { + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + } + }, + "type": "object" + }, + "type": "object" + }, + "keys": { + "items": { + "enum": [ + "add_columns", + "edit_columns", + "filters", + "permissions", + "add_title", + "edit_title", + "none" + ], + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "get_item_schema": { + "properties": { + "columns": { + "items": { + "type": "string" + }, + "type": "array" + }, + "keys": { + "items": { + "enum": [ + "show_columns", + "description_columns", + "label_columns", + "show_title", + "none" + ], + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "get_list_schema": { + "properties": { + "columns": { + "items": { + "type": "string" + }, + "type": "array" + }, + "filters": { + "items": { + "properties": { + "col": { + "type": "string" + }, + "opr": { + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "boolean" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "col", + "opr", + "value" + ], + "type": "object" + }, + "type": "array" + }, + "keys": { + "items": { + "enum": [ + "list_columns", + "order_columns", + "label_columns", + "description_columns", + "list_title", + "none" + ], + "type": "string" + }, + "type": "array" + }, + "order_column": { + "type": "string" + }, + "order_direction": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "select_columns": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "get_recent_activity_schema": { + "properties": { + "actions": { + "items": { + "type": "string" + }, + "type": "array" + }, + "distinct": { + "type": "boolean" + }, + "page": { + "type": "number" + }, + "page_size": { + "type": "number" + } + }, + "type": "object" + }, + "get_related_schema": { + "properties": { + "filter": { + "type": "string" + }, + "include_ids": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + } + }, + "type": "object" + }, + "queries_get_updated_since_schema": { + "properties": { + "last_updated_ms": { + "type": "number" + } + }, + "required": [ + "last_updated_ms" + ], + "type": "object" + }, + "screenshot_query_schema": { + "properties": { + "force": { + "type": "boolean" + }, + "thumb_size": { + "items": { + "type": "integer" + }, + "type": "array" + }, + "window_size": { + "items": { + "type": "integer" + }, + "type": "array" + } + }, + "type": "object" + }, + "sql_lab_get_results_schema": { + "properties": { + "key": { + "type": "string" + } + }, + "required": [ + "key" + ], + "type": "object" + }, + "thumbnail_query_schema": { + "properties": { + "force": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "jwt": { + "bearerFormat": "JWT", + "scheme": "bearer", + "type": "http" + }, + "jwt_refresh": { + "bearerFormat": "JWT", + "scheme": "bearer", + "type": "http" + } + } + }, + "info": { + "description": "Superset", + "title": "Superset", + "version": "v1" + }, + "openapi": "3.0.2", + "paths": { + "/api/v1/advanced_data_type/convert": { + "get": { + "description": "Returns an AdvancedDataTypeResponse object populated with the passed in args.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/advanced_data_type_convert_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdvancedDataTypeSchema" + } + } + }, + "description": "AdvancedDataTypeResponse object has been returned." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Return an AdvancedDataTypeResponse", + "tags": [ + "Advanced Data Type" + ] + } + }, + "/api/v1/advanced_data_type/types": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "a successful return of the available advanced data types has taken place." + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Return a list of available advanced data types", + "tags": [ + "Advanced Data Type" + ] + } + }, + "/api/v1/annotation_layer/": { + "delete": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_delete_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "CSS templates bulk delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete multiple annotation layers in a bulk operation", + "tags": [ + "Annotation Layers" + ] + }, + "get": { + "description": "Gets a list of annotation layers, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/AnnotationLayerRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of annotation layers", + "tags": [ + "Annotation Layers" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationLayerRestApi.post" + } + } + }, + "description": "Annotation Layer schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/AnnotationLayerRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Annotation added" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create an annotation layer", + "tags": [ + "Annotation Layers" + ] + } + }, + "/api/v1/annotation_layer/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get metadata information about this API resource", + "tags": [ + "Annotation Layers" + ] + } + }, + "/api/v1/annotation_layer/related/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "Annotation Layers" + ] + } + }, + "/api/v1/annotation_layer/{pk}": { + "delete": { + "parameters": [ + { + "description": "The annotation layer pk for this annotation", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete annotation layer", + "tags": [ + "Annotation Layers" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/AnnotationLayerRestApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get an annotation layer", + "tags": [ + "Annotation Layers" + ] + }, + "put": { + "parameters": [ + { + "description": "The annotation layer pk for this annotation", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationLayerRestApi.put" + } + } + }, + "description": "Annotation schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/AnnotationLayerRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Annotation changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update an annotation layer", + "tags": [ + "Annotation Layers" + ] + } + }, + "/api/v1/annotation_layer/{pk}/annotation/": { + "delete": { + "parameters": [ + { + "description": "The annotation layer pk for this annotation", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_delete_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Annotations bulk delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Bulk delete annotation layers", + "tags": [ + "Annotation Layers" + ] + }, + "get": { + "description": "Gets a list of annotation layers, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "description": "The annotation layer id for this annotation", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "ids": { + "description": "A list of annotation ids", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/AnnotationRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Annotations" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of annotation layers", + "tags": [ + "Annotation Layers" + ] + }, + "post": { + "parameters": [ + { + "description": "The annotation layer pk for this annotation", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationRestApi.post" + } + } + }, + "description": "Annotation schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/AnnotationRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Annotation added" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create an annotation layer", + "tags": [ + "Annotation Layers" + ] + } + }, + "/api/v1/annotation_layer/{pk}/annotation/{annotation_id}": { + "delete": { + "parameters": [ + { + "description": "The annotation layer pk for this annotation", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "The annotation pk for this annotation", + "in": "path", + "name": "annotation_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete annotation layer", + "tags": [ + "Annotation Layers" + ] + }, + "get": { + "parameters": [ + { + "description": "The annotation layer pk for this annotation", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "The annotation pk", + "in": "path", + "name": "annotation_id", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The item id", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/AnnotationRestApi.get" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get an annotation layer", + "tags": [ + "Annotation Layers" + ] + }, + "put": { + "parameters": [ + { + "description": "The annotation layer pk for this annotation", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "The annotation pk for this annotation", + "in": "path", + "name": "annotation_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationRestApi.put" + } + } + }, + "description": "Annotation schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/AnnotationRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Annotation changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update an annotation layer", + "tags": [ + "Annotation Layers" + ] + } + }, + "/api/v1/assets/export/": { + "get": { + "description": "Gets a ZIP file with all the Superset assets (databases, datasets, charts, dashboards, saved queries) as YAML files.", + "responses": { + "200": { + "content": { + "application/zip": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "ZIP file" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Export all assets", + "tags": [ + "Import/export" + ] + } + }, + "/api/v1/assets/import/": { + "post": { + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "bundle": { + "description": "upload file (ZIP or JSON)", + "format": "binary", + "type": "string" + }, + "passwords": { + "description": "JSON map of passwords for each featured database in the ZIP file. If the ZIP includes a database config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "sparse": { + "description": "allow sparse update of resources", + "type": "boolean" + }, + "ssh_tunnel_passwords": { + "description": "JSON map of passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_key_passwords": { + "description": "JSON map of private_key_passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_keys": { + "description": "JSON map of private_keys for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key\"}`.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Assets import result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Import multiple assets", + "tags": [ + "Import/export" + ] + } + }, + "/api/v1/async_event/": { + "get": { + "description": "Reads off of the Redis events stream, using the user's JWT token and optional query params for last event received.", + "parameters": [ + { + "description": "Last ID received by the client", + "in": "query", + "name": "last_id", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "items": { + "properties": { + "channel_id": { + "type": "string" + }, + "errors": { + "items": { + "type": "object" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "job_id": { + "type": "string" + }, + "result_url": { + "type": "string" + }, + "status": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Async event results" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Read off of the Redis events stream", + "tags": [ + "AsyncEventsRestApi" + ] + } + }, + "/api/v1/available_domains/": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/AvailableDomainsSchema" + } + }, + "type": "object" + } + } + }, + "description": "a list of available domains" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get all available domains", + "tags": [ + "Available Domains" + ] + } + }, + "/api/v1/cachekey/invalidate": { + "post": { + "description": "Takes a list of datasources, finds and invalidates the associated cache records and removes the database records.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CacheInvalidationRequestSchema" + } + } + }, + "description": "A list of datasources uuid or the tuples of database and datasource names", + "required": true + }, + "responses": { + "201": { + "description": "cache was successfully invalidated" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Invalidate cache records and remove the database records", + "tags": [ + "CacheRestApi" + ] + } + }, + "/api/v1/chart/": { + "delete": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_delete_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Charts bulk delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Bulk delete charts", + "tags": [ + "Charts" + ] + }, + "get": { + "description": "Gets a list of charts, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/ChartRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of charts", + "tags": [ + "Charts" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartRestApi.post" + } + } + }, + "description": "Chart schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/ChartRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Chart added" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a new chart", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get metadata information about this API resource", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/data": { + "post": { + "description": "Takes a query context constructed in the client and returns payload data response for the given query.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartDataQueryContextSchema" + } + } + }, + "description": "A query context consists of a datasource from which to fetch data and one or many query objects.", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartDataResponseSchema" + } + } + }, + "description": "Query result" + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartDataAsyncResponseSchema" + } + } + }, + "description": "Async job details" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Return payload data response for the given query", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/data/{cache_key}": { + "get": { + "description": "Takes a query context cache key and returns payload data response for the given query.", + "parameters": [ + { + "in": "path", + "name": "cache_key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartDataResponseSchema" + } + } + }, + "description": "Query result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Return payload data response for the given query", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/export/": { + "get": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_export_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/zip": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "A zip file with chart(s), dataset(s) and database(s) as YAML" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Download multiple charts as YAML files", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/favorite_status/": { + "get": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_fav_star_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetFavStarIdsSchema" + } + } + }, + "description": "None" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Check favorited charts for current user", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/import/": { + "post": { + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "formData": { + "description": "upload file (ZIP)", + "format": "binary", + "type": "string" + }, + "overwrite": { + "description": "overwrite existing charts?", + "type": "boolean" + }, + "passwords": { + "description": "JSON map of passwords for each featured database in the ZIP file. If the ZIP includes a database config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_passwords": { + "description": "JSON map of passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_key_passwords": { + "description": "JSON map of private_key_passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_keys": { + "description": "JSON map of private_keys for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key\"}`.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Chart import result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Import chart(s) with associated datasets and databases", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/related/{column_name}": { + "get": { + "description": "Get a list of all possible owners for a chart. Use `owners` has the `column_name` parameter", + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/warm_up_cache": { + "put": { + "description": "Warms up the cache for the chart. Note for slices a force refresh occurs. In terms of the `extra_filters` these can be obtained from records in the JSON encoded `logs.json` column associated with the `explore_json` action.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartCacheWarmUpRequestSchema" + } + } + }, + "description": "Identifies the chart to warm up cache for, and any additional dashboard or filter context to use.", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartCacheWarmUpResponseSchema" + } + } + }, + "description": "Each chart's warmup status" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Warm up the cache for the chart", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Chart delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a chart", + "tags": [ + "Charts" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/ChartRestApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a chart detail information", + "tags": [ + "Charts" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartRestApi.put" + } + } + }, + "description": "Chart schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/ChartRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Chart changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update a chart", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/{pk}/cache_screenshot/": { + "get": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/screenshot_query_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartCacheScreenshotResponseSchema" + } + } + }, + "description": "Chart async result" + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartCacheScreenshotResponseSchema" + } + } + }, + "description": "Chart screenshot task created" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Compute and cache a screenshot", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/{pk}/data/": { + "get": { + "description": "Takes a chart ID and uses the query context stored when the chart was saved to return payload data response.", + "parameters": [ + { + "description": "The chart ID", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "The format in which the data should be returned", + "in": "query", + "name": "format", + "schema": { + "type": "string" + } + }, + { + "description": "The type in which the data should be returned", + "in": "query", + "name": "type", + "schema": { + "type": "string" + } + }, + { + "description": "Should the queries be forced to load from the source", + "in": "query", + "name": "force", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartDataResponseSchema" + } + } + }, + "description": "Query result" + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChartDataAsyncResponseSchema" + } + } + }, + "description": "Async job details" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Return payload data response for a chart", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/{pk}/favorites/": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Chart removed from favorites" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Remove the chart from the user favorite list", + "tags": [ + "Charts" + ] + }, + "post": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Chart added to favorites" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Mark the chart as favorite for the current user", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/{pk}/screenshot/{digest}/": { + "get": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "digest", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "image/*": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Chart screenshot image" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a computed screenshot from cache", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/chart/{pk}/thumbnail/{digest}/": { + "get": { + "description": "Compute or get already computed chart thumbnail from cache.", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "A hex digest that makes this chart unique", + "in": "path", + "name": "digest", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "image/*": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Chart thumbnail image" + }, + "302": { + "description": "Redirects to the current digest" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get chart thumbnail", + "tags": [ + "Charts" + ] + } + }, + "/api/v1/css_template/": { + "delete": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_delete_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "CSS templates bulk delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Bulk delete CSS templates", + "tags": [ + "CSS Templates" + ] + }, + "get": { + "description": "Gets a list of CSS templates, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/CssTemplateRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of CSS templates", + "tags": [ + "CSS Templates" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CssTemplateRestApi.post" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/CssTemplateRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Item inserted" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a CSS template", + "tags": [ + "CSS Templates" + ] + } + }, + "/api/v1/css_template/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get metadata information about this API resource", + "tags": [ + "CSS Templates" + ] + } + }, + "/api/v1/css_template/related/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "CSS Templates" + ] + } + }, + "/api/v1/css_template/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a CSS template", + "tags": [ + "CSS Templates" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/CssTemplateRestApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a CSS template", + "tags": [ + "CSS Templates" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CssTemplateRestApi.put" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/CssTemplateRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Item changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update a CSS template", + "tags": [ + "CSS Templates" + ] + } + }, + "/api/v1/dashboard/": { + "delete": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_delete_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard bulk delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Bulk delete dashboards", + "tags": [ + "Dashboards" + ] + }, + "get": { + "description": "Gets a list of dashboards, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/DashboardRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of dashboards", + "tags": [ + "Dashboards" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DashboardRestApi.post" + } + } + }, + "description": "Dashboard schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/DashboardRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard added" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a new dashboard", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get metadata information about this API resource", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/export/": { + "get": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_export_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + }, + "description": "Dashboard export" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Download multiple dashboards as YAML files", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/favorite_status/": { + "get": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_fav_star_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetFavStarIdsSchema" + } + } + }, + "description": "None" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Check favorited dashboards for current user", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/import/": { + "post": { + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "formData": { + "description": "upload file (ZIP or JSON)", + "format": "binary", + "type": "string" + }, + "overwrite": { + "description": "overwrite existing dashboards?", + "type": "boolean" + }, + "passwords": { + "description": "JSON map of passwords for each featured database in the ZIP file. If the ZIP includes a database config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_passwords": { + "description": "JSON map of passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_key_passwords": { + "description": "JSON map of private_key_passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_keys": { + "description": "JSON map of private_keys for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key\"}`.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard import result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Import dashboard(s) with associated charts/datasets/databases", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/permalink/{key}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "state": { + "description": "The stored state", + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Returns the stored state." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get dashboard's permanent link state", + "tags": [ + "Dashboard Permanent Link" + ] + } + }, + "/api/v1/dashboard/related/{column_name}": { + "get": { + "description": "Get a list of all possible owners for a dashboard.", + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{id_or_slug}": { + "get": { + "parameters": [ + { + "description": "Either the id of the dashboard, or its slug", + "in": "path", + "name": "id_or_slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/DashboardGetResponseSchema" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a dashboard detail information", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{id_or_slug}/charts": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id_or_slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "items": { + "$ref": "#/components/schemas/ChartEntityResponseSchema" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard chart definitions" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a dashboard's chart definitions.", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{id_or_slug}/copy/": { + "post": { + "parameters": [ + { + "description": "The dashboard id or slug", + "in": "path", + "name": "id_or_slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DashboardCopySchema" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "last_modified_time": { + "type": "number" + } + }, + "type": "object" + } + } + }, + "description": "Id of new dashboard and last modified time" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a copy of an existing dashboard", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{id_or_slug}/datasets": { + "get": { + "description": "Returns a list of a dashboard's datasets. Each dataset includes only the information necessary to render the dashboard's charts.", + "parameters": [ + { + "description": "Either the id of the dashboard, or its slug", + "in": "path", + "name": "id_or_slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "items": { + "$ref": "#/components/schemas/DashboardDatasetSchema" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard dataset definitions" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get dashboard's datasets", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{id_or_slug}/embedded": { + "delete": { + "parameters": [ + { + "description": "The dashboard id or slug", + "in": "path", + "name": "id_or_slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successfully removed the configuration" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a dashboard's embedded configuration", + "tags": [ + "Dashboards" + ] + }, + "get": { + "parameters": [ + { + "description": "The dashboard id or slug", + "in": "path", + "name": "id_or_slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/EmbeddedDashboardResponseSchema" + } + }, + "type": "object" + } + } + }, + "description": "Result contains the embedded dashboard config" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get the dashboard's embedded configuration", + "tags": [ + "Dashboards" + ] + }, + "post": { + "parameters": [ + { + "description": "The dashboard id or slug", + "in": "path", + "name": "id_or_slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmbeddedDashboardConfig" + } + } + }, + "description": "The embedded configuration to set", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/EmbeddedDashboardResponseSchema" + } + }, + "type": "object" + } + } + }, + "description": "Successfully set the configuration" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Set a dashboard's embedded configuration", + "tags": [ + "Dashboards" + ] + }, + "put": { + "description": "Sets a dashboard's embedded configuration.", + "parameters": [ + { + "description": "The dashboard id or slug", + "in": "path", + "name": "id_or_slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmbeddedDashboardConfig" + } + } + }, + "description": "The embedded configuration to set", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/EmbeddedDashboardResponseSchema" + } + }, + "type": "object" + } + } + }, + "description": "Successfully set the configuration" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{id_or_slug}/tabs": { + "get": { + "description": "Returns a list of a dashboard's tabs and dashboard's nested tree structure for associated tabs.", + "parameters": [ + { + "description": "Either the id of the dashboard, or its slug", + "in": "path", + "name": "id_or_slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "items": { + "$ref": "#/components/schemas/TabsPayloadSchema" + }, + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard tabs" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get dashboard's tabs", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard deleted" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a dashboard", + "tags": [ + "Dashboards" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DashboardRestApi.put" + } + } + }, + "description": "Dashboard schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "last_modified_time": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/DashboardRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update a dashboard", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{pk}/cache_dashboard_screenshot/": { + "post": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DashboardScreenshotPostSchema" + } + } + } + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DashboardCacheScreenshotResponseSchema" + } + } + }, + "description": "Dashboard async result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Compute and cache a screenshot", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{pk}/colors": { + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "mark_updated", + "schema": { + "description": "Whether to update the dashboard changed_on field", + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DashboardColorsConfigUpdateSchema" + } + } + }, + "description": "Colors configuration", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard colors updated" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update colors configuration for a dashboard.", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{pk}/favorites/": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard removed from favorites" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Remove the dashboard from the user favorite list", + "tags": [ + "Dashboards" + ] + }, + "post": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard added to favorites" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Mark the dashboard as favorite for the current user", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{pk}/filter_state": { + "post": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "tab_id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "examples": { + "numerical_range_filter": { + "description": "**This body should be stringified and put into the value field.**", + "summary": "Numerical Range Filter", + "value": { + "extraFormData": { + "filters": [ + { + "col": "tz_offset", + "op": ">=", + "val": [ + 1000 + ] + }, + { + "col": "tz_offset", + "op": "<=", + "val": [ + 2000 + ] + } + ] + }, + "filterState": { + "label": "1000 <= x <= 2000", + "value": [ + 1000, + 2000 + ] + }, + "id": "NATIVE_FILTER_ID" + } + }, + "time_grain_filter": { + "description": "**This body should be stringified and put into the value field.**", + "summary": "Time Grain Filter", + "value": { + "extraFormData": { + "time_grain_sqla": "P1W/1970-01-03T00:00:00Z" + }, + "filterState": { + "label": "Week ending Saturday", + "value": [ + "P1W/1970-01-03T00:00:00Z" + ] + }, + "id": "NATIVE_FILTER_ID" + } + }, + "time_range_filter": { + "description": "**This body should be stringified and put into the value field.**", + "summary": "Time Range Filter", + "value": { + "extraFormData": { + "time_range": "DATEADD(DATETIME('2025-01-16T00:00:00'), -7, day) : 2025-01-16T00:00:00" + }, + "filterState": { + "value": "DATEADD(DATETIME('2025-01-16T00:00:00'), -7, day) : 2025-01-16T00:00:00" + }, + "id": "NATIVE_FILTER_ID" + } + }, + "timecolumn_filter": { + "description": "**This body should be stringified and put into the value field.**", + "summary": "Time Column Filter", + "value": { + "extraFormData": { + "granularity_sqla": "order_date" + }, + "filterState": { + "value": [ + "order_date" + ] + }, + "id": "NATIVE_FILTER_ID" + } + }, + "value_filter": { + "description": "**This body should be stringified and put into the value field.**", + "summary": "Value Filter", + "value": { + "extraFormData": { + "filters": [ + { + "col": "real_name", + "op": "IN", + "val": [ + "John Doe" + ] + } + ] + }, + "filterState": { + "value": [ + "John Doe" + ] + }, + "id": "NATIVE_FILTER_ID" + } + } + }, + "schema": { + "$ref": "#/components/schemas/TemporaryCachePostSchema" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "key": { + "description": "The key to retrieve the value.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "The value was stored successfully." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a dashboard's filter state", + "tags": [ + "Dashboard Filter State" + ] + } + }, + "/api/v1/dashboard/{pk}/filter_state/{key}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "The value key.", + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "description": "The result of the operation", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Deleted the stored value." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a dashboard's filter state value", + "tags": [ + "Dashboard Filter State" + ] + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "value": { + "description": "The stored value", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Returns the stored value." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a dashboard's filter state value", + "tags": [ + "Dashboard Filter State" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "tab_id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemporaryCachePutSchema" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "key": { + "description": "The key to retrieve the value.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "The value was stored successfully." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update a dashboard's filter state value", + "tags": [ + "Dashboard Filter State" + ] + } + }, + "/api/v1/dashboard/{pk}/filters": { + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DashboardNativeFiltersConfigUpdateSchema" + } + } + }, + "description": "Native filters configuration", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Dashboard native filters updated" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update native filters configuration for a dashboard.", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{pk}/permalink": { + "post": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "examples": { + "numerical_range_filter": { + "summary": "Numerical Range Filter", + "value": { + "dataMask": { + "extraFormData": { + "filters": [ + { + "col": "tz_offset", + "op": ">=", + "val": [ + 1000 + ] + }, + { + "col": "tz_offset", + "op": "<=", + "val": [ + 2000 + ] + } + ] + }, + "filterState": { + "label": "1000 <= x <= 200", + "value": [ + 1000, + 2000 + ] + }, + "id": "NATIVE_FILTER_ID" + } + } + }, + "time_grain_filter": { + "summary": "Time Grain Filter", + "value": { + "dataMask": { + "extraFormData": { + "time_grain_sqla": "P1W/1970-01-03T00:00:00Z" + }, + "filterState": { + "label": "Week ending Saturday", + "value": [ + "P1W/1970-01-03T00:00:00Z" + ] + }, + "id": "NATIVE_FILTER_ID" + } + } + }, + "time_range_filter": { + "summary": "Time Range Filter", + "value": { + "dataMask": { + "extraFormData": { + "time_range": "DATEADD(DATETIME(\"2025-01-16T00:00:00\"), -7, day) : 2025-01-16T00:00:00" + }, + "filterState": { + "value": "DATEADD(DATETIME(\"2025-01-16T00:00:00\"), -7, day) : 2025-01-16T00:00:00" + }, + "id": "NATIVE_FILTER_ID" + } + } + }, + "timecolumn_filter": { + "summary": "Time Column Filter", + "value": { + "dataMask": { + "extraFormData": { + "granularity_sqla": "order_date" + }, + "filterState": { + "value": [ + "order_date" + ] + }, + "id": "NATIVE_FILTER_ID" + } + } + }, + "value_filter": { + "summary": "Value Filter", + "value": { + "dataMask": { + "extraFormData": { + "filters": [ + { + "col": "real_name", + "op": "IN", + "val": [ + "John Doe" + ] + } + ] + }, + "filterState": { + "value": [ + "John Doe" + ] + }, + "id": "NATIVE_FILTER_ID" + } + } + } + }, + "schema": { + "$ref": "#/components/schemas/DashboardPermalinkStateSchema" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "key": { + "description": "The key to retrieve the permanent link data.", + "type": "string" + }, + "url": { + "description": "permanent link.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "The permanent link was stored successfully." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a new dashboard's permanent link", + "tags": [ + "Dashboard Permanent Link" + ] + } + }, + "/api/v1/dashboard/{pk}/screenshot/{digest}/": { + "get": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "digest", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "download_format", + "schema": { + "enum": [ + "png", + "pdf" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "image/*": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Dashboard thumbnail image" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a computed screenshot from cache", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/dashboard/{pk}/thumbnail/{digest}/": { + "get": { + "description": "Computes async or get already computed dashboard thumbnail from cache.", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "A hex digest that makes this dashboard unique", + "in": "path", + "name": "digest", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "image/*": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Dashboard thumbnail image" + }, + "202": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Thumbnail does not exist on cache, fired async to compute" + }, + "302": { + "description": "Redirects to the current digest" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get dashboard's thumbnail", + "tags": [ + "Dashboards" + ] + } + }, + "/api/v1/database/": { + "get": { + "description": "Gets a list of databases, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/DatabaseRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of databases", + "tags": [ + "Database" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseRestApi.post" + } + } + }, + "description": "Database schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/DatabaseRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Database added" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a new database", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get metadata information about this API resource", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/available/": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "properties": { + "available_drivers": { + "description": "Installed drivers for the engine", + "items": { + "type": "string" + }, + "type": "array" + }, + "default_driver": { + "description": "Default driver for the engine", + "type": "string" + }, + "engine": { + "description": "Name of the SQLAlchemy engine", + "type": "string" + }, + "engine_information": { + "description": "Dict with public properties form the DB Engine", + "properties": { + "disable_ssh_tunneling": { + "description": "Whether the engine supports SSH Tunnels", + "type": "boolean" + }, + "supports_file_upload": { + "description": "Whether the engine supports file uploads", + "type": "boolean" + } + }, + "type": "object" + }, + "name": { + "description": "Name of the database", + "type": "string" + }, + "parameters": { + "description": "JSON schema defining the needed parameters", + "type": "object" + }, + "preferred": { + "description": "Is the database preferred?", + "type": "boolean" + }, + "sqlalchemy_uri_placeholder": { + "description": "Example placeholder for the SQLAlchemy URI", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + } + }, + "description": "Database names" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get names of databases currently available", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/export/": { + "get": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_export_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/zip": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "A zip file with database(s) and dataset(s) as YAML" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Download database(s) and associated dataset(s) as a zip file", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/import/": { + "post": { + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "formData": { + "description": "upload file (ZIP)", + "format": "binary", + "type": "string" + }, + "overwrite": { + "description": "overwrite existing databases?", + "type": "boolean" + }, + "passwords": { + "description": "JSON map of passwords for each featured database in the ZIP file. If the ZIP includes a database config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_passwords": { + "description": "JSON map of passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_key_passwords": { + "description": "JSON map of private_key_passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_keys": { + "description": "JSON map of private_keys for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key\"}`.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Database import result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Import database(s) with associated datasets", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/oauth2/": { + "get": { + "description": "-> Receive and store personal access tokens from OAuth for user-level authorization", + "parameters": [ + { + "in": "query", + "name": "state", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "code", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "scope", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "A dummy self-closing HTML page" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Receive personal access tokens from OAuth2", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/related/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/test_connection/": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseTestConnectionSchema" + } + } + }, + "description": "Database schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Database Test Connection" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Test a database connection", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/upload_metadata/": { + "post": { + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/UploadFileMetadataPostSchema" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/UploadFileMetadata" + } + }, + "type": "object" + } + } + }, + "description": "Upload response" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Upload a file and returns file metadata", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/validate_parameters/": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseValidateParametersSchema" + } + } + }, + "description": "DB-specific parameters", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Database Test Connection" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Validate database connection parameters", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Database deleted" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a database", + "tags": [ + "Database" + ] + }, + "get": { + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "Database" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a database", + "tags": [ + "Database" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseRestApi.put" + } + } + }, + "description": "Database schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/DatabaseRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Database changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Change a database", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/catalogs/": { + "get": { + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/database_catalogs_query_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CatalogsResponseSchema" + } + } + }, + "description": "A List of all catalogs from the database" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get all catalogs from a database", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/connection": { + "get": { + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseConnectionSchema" + } + } + }, + "description": "Database with connection info" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a database connection info", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/function_names/": { + "get": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseFunctionNamesResponse" + } + } + }, + "description": "Query result" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get function names supported by a database", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/related_objects/": { + "get": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseRelatedObjectsResponse" + } + } + }, + "description": "Query result" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get charts and dashboards count associated to a database", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/schemas/": { + "get": { + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/database_schemas_query_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchemasResponseSchema" + } + } + }, + "description": "A List of all schemas from the database" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get all schemas from a database", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/schemas_access_for_file_upload/": { + "get": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseSchemaAccessForFileUploadResponse" + } + } + }, + "description": "The list of the database schemas where to upload information" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "The list of the database schemas where to upload information", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/select_star/{table_name}/": { + "get": { + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "Table name", + "in": "path", + "name": "table_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Table schema", + "in": "path", + "name": "schema_name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectStarResponseSchema" + } + } + }, + "description": "SQL statement for a select star for table" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get database select star for table", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/select_star/{table_name}/{schema_name}/": { + "get": { + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "Table name", + "in": "path", + "name": "table_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Table schema", + "in": "path", + "name": "schema_name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectStarResponseSchema" + } + } + }, + "description": "SQL statement for a select star for table" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get database select star for table", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/ssh_tunnel/": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "SSH Tunnel deleted" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a SSH tunnel", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/sync_permissions/": { + "post": { + "parameters": [ + { + "description": "The database connection ID", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Task created to sync permissions." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Re-sync all permissions for a database connection", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/table/{table_name}/{schema_name}/": { + "get": { + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "Table name", + "in": "path", + "name": "table_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Table schema", + "in": "path", + "name": "schema_name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TableMetadataResponseSchema" + } + } + }, + "description": "Table metadata information" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get database table metadata", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/table_extra/{table_name}/{schema_name}/": { + "get": { + "description": "Response depends on each DB engine spec normally focused on partitions.", + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "Table name", + "in": "path", + "name": "table_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Table schema", + "in": "path", + "name": "schema_name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TableExtraMetadataResponseSchema" + } + } + }, + "description": "Table extra metadata information" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get table extra metadata", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/table_metadata/": { + "get": { + "description": "Metadata associated with the table (columns, indexes, etc.)", + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "Table name", + "in": "query", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Optional table schema, if not passed default schema will be used", + "in": "query", + "name": "schema", + "schema": { + "type": "string" + } + }, + { + "description": "Optional table catalog, if not passed default catalog will be used", + "in": "query", + "name": "catalog", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TableExtraMetadataResponseSchema" + } + } + }, + "description": "Table metadata information" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get table metadata", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/table_metadata/extra/": { + "get": { + "description": "Extra metadata associated with the table (partitions, description, etc.)", + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "Table name", + "in": "query", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Optional table schema, if not passed the schema configured in the database will be used", + "in": "query", + "name": "schema", + "schema": { + "type": "string" + } + }, + { + "description": "Optional table catalog, if not passed the catalog configured in the database will be used", + "in": "query", + "name": "catalog", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TableExtraMetadataResponseSchema" + } + } + }, + "description": "Table extra metadata information" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get table extra metadata", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/tables/": { + "get": { + "parameters": [ + { + "description": "The database id", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/database_tables_query_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "type": "integer" + }, + "result": { + "description": "A List of tables for given database", + "items": { + "$ref": "#/components/schemas/DatabaseTablesResponse" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Tables list" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of tables for given database", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/upload/": { + "post": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/UploadPostSchema" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "CSV upload response" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Upload a file to a database table", + "tags": [ + "Database" + ] + } + }, + "/api/v1/database/{pk}/validate_sql/": { + "post": { + "description": "Validates that arbitrary SQL is acceptable for the given database.", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateSQLRequest" + } + } + }, + "description": "Validate SQL request", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "description": "A List of SQL errors found on the statement", + "items": { + "$ref": "#/components/schemas/ValidateSQLResponse" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Validation result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Validate arbitrary SQL", + "tags": [ + "Database" + ] + } + }, + "/api/v1/dataset/": { + "delete": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_delete_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Dataset bulk delete" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Bulk delete datasets", + "tags": [ + "Datasets" + ] + }, + "get": { + "description": "Gets a list of datasets, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/DatasetRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of datasets", + "tags": [ + "Datasets" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatasetRestApi.post" + } + } + }, + "description": "Dataset schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/DatasetRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Dataset added" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a new dataset", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get metadata information about this API resource", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/distinct/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DistincResponseSchema" + } + } + }, + "description": "Distinct field data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get distinct values from field data", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/duplicate": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatasetDuplicateSchema" + } + } + }, + "description": "Dataset schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/DatasetDuplicateSchema" + } + }, + "type": "object" + } + } + }, + "description": "Dataset duplicated" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Duplicate a dataset", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/export/": { + "get": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_export_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + }, + "description": "Dataset export" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Download multiple datasets as YAML files", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/get_or_create/": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrCreateDatasetSchema" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "properties": { + "table_id": { + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "The ID of the table" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Retrieve a table by name, or create it if it does not exist", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/import/": { + "post": { + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "formData": { + "description": "upload file (ZIP or YAML)", + "format": "binary", + "type": "string" + }, + "overwrite": { + "description": "overwrite existing datasets?", + "type": "boolean" + }, + "passwords": { + "description": "JSON map of passwords for each featured database in the ZIP file. If the ZIP includes a database config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_passwords": { + "description": "JSON map of passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_key_passwords": { + "description": "JSON map of private_key_passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_keys": { + "description": "JSON map of private_keys for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key\"}`.", + "type": "string" + }, + "sync_columns": { + "description": "sync columns?", + "type": "boolean" + }, + "sync_metrics": { + "description": "sync metrics?", + "type": "boolean" + } + }, + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Dataset import result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Import dataset(s) with associated databases", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/related/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/warm_up_cache": { + "put": { + "description": "Warms up the cache for the table. Note for slices a force refresh occurs. In terms of the `extra_filters` these can be obtained from records in the JSON encoded `logs.json` column associated with the `explore_json` action.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatasetCacheWarmUpRequestSchema" + } + } + }, + "description": "Identifies the database and table to warm up cache for, and any additional dashboard or filter context to use.", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatasetCacheWarmUpResponseSchema" + } + } + }, + "description": "Each chart's warmup status" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Warm up the cache for each chart powered by the given table", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Dataset delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a dataset", + "tags": [ + "Datasets" + ] + }, + "get": { + "description": "Get a dataset by ID", + "parameters": [ + { + "description": "The dataset ID", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + }, + { + "description": "Should Jinja macros from sql, metrics and columns be rendered and included in the response", + "in": "query", + "name": "include_rendered_sql", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The item id", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/DatasetRestApi.get" + } + }, + "type": "object" + } + } + }, + "description": "Dataset object has been returned." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a dataset", + "tags": [ + "Datasets" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "override_columns", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatasetRestApi.put" + } + } + }, + "description": "Dataset schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/DatasetRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Dataset changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update a dataset", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/{pk}/column/{column_id}": { + "delete": { + "parameters": [ + { + "description": "The dataset pk for this column", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "The column id for this dataset", + "in": "path", + "name": "column_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Column deleted" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a dataset column", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/{pk}/metric/{metric_id}": { + "delete": { + "parameters": [ + { + "description": "The dataset pk for this column", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "The metric id for this dataset", + "in": "path", + "name": "metric_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Metric deleted" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a dataset metric", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/{pk}/refresh": { + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Dataset delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Refresh and update columns of a dataset", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/dataset/{pk}/related_objects": { + "get": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatasetRelatedObjectsResponse" + } + } + }, + "description": "Query result" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get charts and dashboards count associated to a dataset", + "tags": [ + "Datasets" + ] + } + }, + "/api/v1/datasource/{datasource_type}/{datasource_id}/column/{column_name}/values/": { + "get": { + "parameters": [ + { + "description": "The type of datasource", + "in": "path", + "name": "datasource_type", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The id of the datasource", + "in": "path", + "name": "datasource_id", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "The name of the column to get values for", + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + } + ] + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "A List of distinct values for the column" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get possible values for a datasource column", + "tags": [ + "Datasources" + ] + } + }, + "/api/v1/embedded_dashboard/{uuid}": { + "get": { + "parameters": [ + { + "description": "The embedded configuration uuid", + "in": "path", + "name": "uuid", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The ui config of embedded dashboard (optional).", + "in": "query", + "name": "uiConfig", + "schema": { + "type": "number" + } + }, + { + "description": "Show filters (optional).", + "in": "query", + "name": "show_filters", + "schema": { + "type": "boolean" + } + }, + { + "description": "Expand filters (optional).", + "in": "query", + "name": "expand_filters", + "schema": { + "type": "boolean" + } + }, + { + "description": "Native filters key to apply filters. (optional).", + "in": "query", + "name": "native_filters_key", + "schema": { + "type": "string" + } + }, + { + "description": "Permalink key to apply filters. (optional).", + "in": "query", + "name": "permalink_key", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/EmbeddedDashboardResponseSchema" + } + }, + "type": "object" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "Result contains the embedded dashboard configuration" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a report schedule log", + "tags": [ + "Embedded Dashboard" + ] + } + }, + "/api/v1/explore/": { + "get": { + "description": "Assembles Explore related information (form_data, slice, dataset) in a single endpoint.

The information can be assembled from:
- The cache using a form_data_key
- The metadata database using a permalink_key
- Build from scratch using dataset or slice identifiers.", + "parameters": [ + { + "in": "query", + "name": "form_data_key", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "permalink_key", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "slice_id", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "datasource_id", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "datasource_type", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExploreContextSchema" + } + } + }, + "description": "Returns the initial context." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Assemble Explore related information in a single endpoint", + "tags": [ + "Explore" + ] + } + }, + "/api/v1/explore/form_data": { + "post": { + "parameters": [ + { + "in": "query", + "name": "tab_id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormDataPostSchema" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "key": { + "description": "The key to retrieve the form_data.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "The form_data was stored successfully." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a new form_data", + "tags": [ + "Explore Form Data" + ] + } + }, + "/api/v1/explore/form_data/{key}": { + "delete": { + "parameters": [ + { + "description": "The form_data key.", + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "description": "The result of the operation", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Deleted the stored form_data." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a form_data", + "tags": [ + "Explore Form Data" + ] + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "form_data": { + "description": "The stored form_data", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Returns the stored form_data." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a form_data", + "tags": [ + "Explore Form Data" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "tab_id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormDataPutSchema" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "key": { + "description": "The key to retrieve the form_data.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "The form_data was stored successfully." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update an existing form_data", + "tags": [ + "Explore Form Data" + ] + } + }, + "/api/v1/explore/permalink": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExplorePermalinkStateSchema" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "key": { + "description": "The key to retrieve the permanent link data.", + "type": "string" + }, + "url": { + "description": "permanent link.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "The permanent link was stored successfully." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a new permanent link", + "tags": [ + "Explore Permanent Link" + ] + } + }, + "/api/v1/explore/permalink/{key}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "state": { + "description": "The stored state", + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Returns the stored form_data." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get chart's permanent link state", + "tags": [ + "Explore Permanent Link" + ] + } + }, + "/api/v1/log/": { + "get": { + "description": "Gets a list of logs, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/LogRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of logs", + "tags": [ + "LogRestApi" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LogRestApi.post" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/LogRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Item inserted" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "LogRestApi" + ] + } + }, + "/api/v1/log/recent_activity/": { + "get": { + "parameters": [ + { + "description": "The id of the user", + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_recent_activity_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecentActivityResponseSchema" + } + } + }, + "description": "A List of recent activity objects" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get recent activity data for a user", + "tags": [ + "LogRestApi" + ] + } + }, + "/api/v1/log/{pk}": { + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/LogRestApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a log detail information", + "tags": [ + "LogRestApi" + ] + } + }, + "/api/v1/me/": { + "get": { + "description": "Gets the user object corresponding to the agent making the request, or returns a 401 error if the user is unauthenticated.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/UserResponseSchema" + } + }, + "type": "object" + } + } + }, + "description": "The current user" + }, + "401": { + "$ref": "#/components/responses/401" + } + }, + "summary": "Get the user object", + "tags": [ + "Current User" + ] + } + }, + "/api/v1/me/roles/": { + "get": { + "description": "Gets the user roles corresponding to the agent making the request, or returns a 401 error if the user is unauthenticated.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/UserResponseSchema" + } + }, + "type": "object" + } + } + }, + "description": "The current user" + }, + "401": { + "$ref": "#/components/responses/401" + } + }, + "summary": "Get the user roles", + "tags": [ + "Current User" + ] + } + }, + "/api/v1/menu/": { + "get": { + "description": "Get the menu data structure. Returns a forest like structure with the menu the user has access to", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "description": "Menu items in a forest like data structure", + "items": { + "properties": { + "childs": { + "items": { + "type": "object" + }, + "type": "array" + }, + "icon": { + "description": "Icon name to show for this menu item", + "type": "string" + }, + "label": { + "description": "Pretty name for the menu item", + "type": "string" + }, + "name": { + "description": "The internal menu item name, maps to permission_name", + "type": "string" + }, + "url": { + "description": "The URL for the menu item", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Get menu data" + }, + "401": { + "$ref": "#/components/responses/401" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Menu" + ] + } + }, + "/api/v1/query/": { + "get": { + "description": "Gets a list of queries, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/QueryRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of queries", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/query/distinct/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DistincResponseSchema" + } + } + }, + "description": "Distinct field data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get distinct values from field data", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/query/related/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/query/stop": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StopQuerySchema" + } + } + }, + "description": "Stop query schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Query stopped" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Manually stop a query with client_id", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/query/updated_since": { + "get": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/queries_get_updated_since_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "description": "A List of queries that changed after last_updated_ms", + "items": { + "$ref": "#/components/schemas/QueryRestApi.get" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Queries list" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of queries that changed after last_updated_ms", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/query/{pk}": { + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/QueryRestApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get query detail information", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/report/": { + "delete": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_delete_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Report Schedule bulk delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Bulk delete report schedules", + "tags": [ + "Report Schedules" + ] + }, + "get": { + "description": "Gets a list of report schedules, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of report schedules", + "tags": [ + "Report Schedules" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReportScheduleRestApi.post" + } + } + }, + "description": "Report Schedule schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/ReportScheduleRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Report schedule added" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a report schedule", + "tags": [ + "Report Schedules" + ] + } + }, + "/api/v1/report/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get metadata information about this API resource", + "tags": [ + "Report Schedules" + ] + } + }, + "/api/v1/report/related/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "Report Schedules" + ] + } + }, + "/api/v1/report/slack_channels/": { + "get": { + "description": "Get slack channels", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_slack_channels_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "items": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Slack channels" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get slack channels", + "tags": [ + "Report Schedules" + ] + } + }, + "/api/v1/report/{pk}": { + "delete": { + "parameters": [ + { + "description": "The report schedule pk", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a report schedule", + "tags": [ + "Report Schedules" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/ReportScheduleRestApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a report schedule", + "tags": [ + "Report Schedules" + ] + }, + "put": { + "parameters": [ + { + "description": "The Report Schedule pk", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReportScheduleRestApi.put" + } + } + }, + "description": "Report Schedule schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/ReportScheduleRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Report Schedule changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update a report schedule", + "tags": [ + "Report Schedules" + ] + } + }, + "/api/v1/report/{pk}/log/": { + "get": { + "description": "Gets a list of report schedule logs, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "description": "The report schedule id for these logs", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "ids": { + "description": "A list of log ids", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/ReportExecutionLogRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from logs" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of report schedule logs", + "tags": [ + "Report Schedules" + ] + } + }, + "/api/v1/report/{pk}/log/{log_id}": { + "get": { + "parameters": [ + { + "description": "The report schedule pk for log", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "The log pk", + "in": "path", + "name": "log_id", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The log id", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/ReportExecutionLogRestApi.get" + } + }, + "type": "object" + } + } + }, + "description": "Item log" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a report schedule log", + "tags": [ + "Report Schedules" + ] + } + }, + "/api/v1/rowlevelsecurity/": { + "delete": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_delete_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "RLS Rule bulk delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Bulk delete RLS rules", + "tags": [ + "Row Level Security" + ] + }, + "get": { + "description": "Gets a list of RLS, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/RLSRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of RLS", + "tags": [ + "Row Level Security" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RLSRestApi.post" + } + } + }, + "description": "RLS schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/RLSRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "RLS Rule added" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a new RLS rule", + "tags": [ + "Row Level Security" + ] + } + }, + "/api/v1/rowlevelsecurity/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get metadata information about this API resource", + "tags": [ + "Row Level Security" + ] + } + }, + "/api/v1/rowlevelsecurity/related/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "Row Level Security" + ] + } + }, + "/api/v1/rowlevelsecurity/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete an RLS", + "tags": [ + "Row Level Security" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/RLSRestApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get an RLS", + "tags": [ + "Row Level Security" + ] + }, + "put": { + "parameters": [ + { + "description": "The Rule pk", + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RLSRestApi.put" + } + } + }, + "description": "RLS schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/RLSRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Rule changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update an RLS rule", + "tags": [ + "Row Level Security" + ] + } + }, + "/api/v1/saved_query/": { + "delete": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_delete_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Saved queries bulk delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Bulk delete saved queries", + "tags": [ + "Queries" + ] + }, + "get": { + "description": "Gets a list of saved queries, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/SavedQueryRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of saved queries", + "tags": [ + "Queries" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SavedQueryRestApi.post" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/SavedQueryRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Item inserted" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a saved query", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/saved_query/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get metadata information about this API resource", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/saved_query/distinct/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DistincResponseSchema" + } + } + }, + "description": "Distinct field data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get distinct values from field data", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/saved_query/export/": { + "get": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_export_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/zip": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "A zip file with saved query(ies) and database(s) as YAML" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Download multiple saved queries as YAML files", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/saved_query/import/": { + "post": { + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "formData": { + "description": "upload file (ZIP)", + "format": "binary", + "type": "string" + }, + "overwrite": { + "description": "overwrite existing saved queries?", + "type": "boolean" + }, + "passwords": { + "description": "JSON map of passwords for each featured database in the ZIP file. If the ZIP includes a database config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_passwords": { + "description": "JSON map of passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the password should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_key_passwords": { + "description": "JSON map of private_key_passwords for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key_password\"}`.", + "type": "string" + }, + "ssh_tunnel_private_keys": { + "description": "JSON map of private_keys for each ssh_tunnel associated to a featured database in the ZIP file. If the ZIP includes a ssh_tunnel config in the path `databases/MyDatabase.yaml`, the private_key should be provided in the following format: `{\"databases/MyDatabase.yaml\": \"my_private_key\"}`.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Saved Query import result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Import saved queries with associated databases", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/saved_query/related/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/saved_query/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a saved query", + "tags": [ + "Queries" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/SavedQueryRestApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a saved query", + "tags": [ + "Queries" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SavedQueryRestApi.put" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/SavedQueryRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Item changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update a saved query", + "tags": [ + "Queries" + ] + } + }, + "/api/v1/security/csrf_token/": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Result contains the CSRF token" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get the CSRF token", + "tags": [ + "Security" + ] + } + }, + "/api/v1/security/guest_token/": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuestTokenCreate" + } + } + }, + "description": "Parameters for the guest token", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "token": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Result contains the guest token" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a guest token", + "tags": [ + "Security" + ] + } + }, + "/api/v1/security/login": { + "post": { + "description": "Authenticate and get a JWT access and refresh token", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "password": { + "description": "The password for authentication", + "example": "complex-password", + "type": "string" + }, + "provider": { + "description": "Choose an authentication provider", + "enum": [ + "db", + "ldap" + ], + "example": "db", + "type": "string" + }, + "refresh": { + "description": "If true a refresh token is provided also", + "example": true, + "type": "boolean" + }, + "username": { + "description": "The username for authentication", + "example": "admin", + "type": "string" + } + }, + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Authentication Successful" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "tags": [ + "Security" + ] + } + }, + "/api/v1/security/permissions-resources/": { + "get": { + "description": "Get a list of models", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/PermissionViewMenuApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Permissions on Resources (View Menus)" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionViewMenuApi.post" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/PermissionViewMenuApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Item inserted" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Permissions on Resources (View Menus)" + ] + } + }, + "/api/v1/security/permissions-resources/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Permissions on Resources (View Menus)" + ] + } + }, + "/api/v1/security/permissions-resources/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Permissions on Resources (View Menus)" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/PermissionViewMenuApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Permissions on Resources (View Menus)" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionViewMenuApi.put" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/PermissionViewMenuApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Item changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Permissions on Resources (View Menus)" + ] + } + }, + "/api/v1/security/permissions/": { + "get": { + "description": "Get a list of models", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/PermissionApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Permissions" + ] + } + }, + "/api/v1/security/permissions/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Permissions" + ] + } + }, + "/api/v1/security/permissions/{pk}": { + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/PermissionApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Permissions" + ] + } + }, + "/api/v1/security/refresh": { + "post": { + "description": "Use the refresh token to get a new JWT access token", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "access_token": { + "description": "A new refreshed access token", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Refresh Successful" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt_refresh": [] + } + ], + "tags": [ + "Security" + ] + } + }, + "/api/v1/security/resources/": { + "get": { + "description": "Get a list of models", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/ViewMenuApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Resources (View Menus)" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewMenuApi.post" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/ViewMenuApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Item inserted" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Resources (View Menus)" + ] + } + }, + "/api/v1/security/resources/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Resources (View Menus)" + ] + } + }, + "/api/v1/security/resources/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Resources (View Menus)" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/ViewMenuApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Resources (View Menus)" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewMenuApi.put" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/ViewMenuApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Item changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Resources (View Menus)" + ] + } + }, + "/api/v1/security/roles/": { + "get": { + "description": "Get a list of models", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/SupersetRoleApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Roles" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupersetRoleApi.post" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/SupersetRoleApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Item inserted" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Roles" + ] + } + }, + "/api/v1/security/roles/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Roles" + ] + } + }, + "/api/v1/security/roles/search/": { + "get": { + "description": "Fetch a paginated list of roles with user and permission IDs.", + "parameters": [ + { + "in": "query", + "name": "q", + "schema": { + "properties": { + "filters": { + "items": { + "properties": { + "col": { + "enum": [ + "user_ids", + "permission_ids", + "name" + ], + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "order_column": { + "default": "id", + "enum": [ + "id", + "name" + ], + "type": "string" + }, + "order_direction": { + "default": "asc", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "default": 0, + "type": "integer" + }, + "page_size": { + "default": 10, + "type": "integer" + } + }, + "type": "object" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RolesResponseSchema" + } + } + }, + "description": "Successfully retrieved roles" + }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Bad request (invalid input)" + }, + "403": { + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Forbidden" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "List roles", + "tags": [ + "Security Roles" + ] + } + }, + "/api/v1/security/roles/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Roles" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/SupersetRoleApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Roles" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupersetRoleApi.put" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/SupersetRoleApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Item changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Roles" + ] + } + }, + "/api/v1/security/roles/{role_id}/permissions": { + "post": { + "parameters": [ + { + "in": "path", + "name": "role_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RolePermissionPostSchema" + } + } + }, + "description": "Add role permissions schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/RolePermissionPostSchema" + } + }, + "type": "object" + } + } + }, + "description": "Permissions added" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Roles" + ] + } + }, + "/api/v1/security/roles/{role_id}/permissions/": { + "get": { + "parameters": [ + { + "in": "path", + "name": "role_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/RolePermissionListSchema" + } + }, + "type": "object" + } + } + }, + "description": "List of permissions" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Roles" + ] + } + }, + "/api/v1/security/roles/{role_id}/users": { + "put": { + "parameters": [ + { + "in": "path", + "name": "role_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleUserPutSchema" + } + } + }, + "description": "Update role users schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/RoleUserPutSchema" + } + }, + "type": "object" + } + } + }, + "description": "Role users updated" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Roles" + ] + } + }, + "/api/v1/security/users/": { + "get": { + "description": "Get a list of models", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/SupersetUserApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Users" + ] + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupersetUserApi.post" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/SupersetUserApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Item changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Users" + ] + } + }, + "/api/v1/security/users/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Users" + ] + } + }, + "/api/v1/security/users/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Users" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/SupersetUserApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Users" + ] + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupersetUserApi.put" + } + } + }, + "description": "Model schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "$ref": "#/components/schemas/SupersetUserApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Item changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Security Users" + ] + } + }, + "/api/v1/sqllab/": { + "get": { + "description": "Assembles SQLLab bootstrap data (active_tab, databases, queries, tab_state_ids) in a single endpoint. The data can be assembled from the current user's id.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SQLLabBootstrapSchema" + } + } + }, + "description": "Returns the initial bootstrap data for SqlLab" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get the bootstrap data for SqlLab page", + "tags": [ + "SQL Lab" + ] + } + }, + "/api/v1/sqllab/estimate/": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EstimateQueryCostSchema" + } + } + }, + "description": "SQL query and params", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Query estimation result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Estimate the SQL query execution cost", + "tags": [ + "SQL Lab" + ] + } + }, + "/api/v1/sqllab/execute/": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExecutePayloadSchema" + } + } + }, + "description": "SQL query and params", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryExecutionResponseSchema" + } + } + }, + "description": "Query execution result" + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryExecutionResponseSchema" + } + } + }, + "description": "Query execution result, query still running" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Execute a SQL query", + "tags": [ + "SQL Lab" + ] + } + }, + "/api/v1/sqllab/export/{client_id}/": { + "get": { + "parameters": [ + { + "description": "The SQL query result identifier", + "in": "path", + "name": "client_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "text/csv": { + "schema": { + "type": "string" + } + } + }, + "description": "SQL query results" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Export the SQL query results to a CSV", + "tags": [ + "SQL Lab" + ] + } + }, + "/api/v1/sqllab/format_sql/": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormatQueryPayloadSchema" + } + } + }, + "description": "SQL query", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Format SQL result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Format SQL code", + "tags": [ + "SQL Lab" + ] + } + }, + "/api/v1/sqllab/permalink": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExplorePermalinkStateSchema" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "key": { + "description": "The key to retrieve the permanent link data.", + "type": "string" + }, + "url": { + "description": "permanent link.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "The permanent link was stored successfully." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a new permanent link", + "tags": [ + "SQL Lab Permanent Link" + ] + } + }, + "/api/v1/sqllab/permalink/{key}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "state": { + "description": "The stored state", + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Returns the stored form_data." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get permanent link state for SQLLab editor.", + "tags": [ + "SQL Lab Permanent Link" + ] + } + }, + "/api/v1/sqllab/results/": { + "get": { + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sql_lab_get_results_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryExecutionResponseSchema" + } + } + }, + "description": "SQL query execution result" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "410": { + "$ref": "#/components/responses/410" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get the result of a SQL query execution", + "tags": [ + "SQL Lab" + ] + } + }, + "/api/v1/tag/": { + "delete": { + "description": "Bulk deletes tags. This will remove all tagged objects with this tag.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/delete_tags_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Deletes multiple Tags" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Bulk delete tags", + "tags": [ + "Tags" + ] + }, + "get": { + "description": "Get a list of tags, use Rison or JSON query parameters for filtering, sorting, pagination and for selecting specific columns and metadata.", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_list_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "description": "The total record count on the backend", + "type": "number" + }, + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "ids": { + "description": "A list of item ids, useful when you don't know the column id", + "items": { + "type": "string" + }, + "type": "array" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "list_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "list_title": { + "description": "A title to render. Will be translated by babel", + "example": "List Items", + "type": "string" + }, + "order_columns": { + "description": "A list of allowed columns to sort", + "items": { + "type": "string" + }, + "type": "array" + }, + "result": { + "description": "The result from the get list query", + "items": { + "$ref": "#/components/schemas/TagRestApi.get_list" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Items from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a list of tags", + "tags": [ + "Tags" + ] + }, + "post": { + "description": "Create a new Tag", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagRestApi.post" + } + } + }, + "description": "Tag schema", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/TagRestApi.post" + } + }, + "type": "object" + } + } + }, + "description": "Tag added" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Create a tag", + "tags": [ + "Tags" + ] + } + }, + "/api/v1/tag/_info": { + "get": { + "description": "Get metadata information about this API resource", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_info_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "add_columns": { + "type": "object" + }, + "edit_columns": { + "type": "object" + }, + "filters": { + "properties": { + "column_name": { + "items": { + "properties": { + "name": { + "description": "The filter name. Will be translated by babel", + "type": "string" + }, + "operator": { + "description": "The filter operation key to use on list filters", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "permissions": { + "description": "The user permissions for this API resource", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get metadata information about tag API endpoints", + "tags": [ + "Tags" + ] + } + }, + "/api/v1/tag/bulk_create": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagPostBulkSchema" + } + } + }, + "description": "Tag schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagPostBulkResponseSchema" + } + } + }, + "description": "Bulk created tags and tagged objects" + }, + "302": { + "description": "Redirects to the current digest" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Bulk create tags and tagged objects", + "tags": [ + "Tags" + ] + } + }, + "/api/v1/tag/favorite_status/": { + "get": { + "description": "Get favorited tags for current user", + "parameters": [ + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_fav_star_ids_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetFavStarIdsSchema" + } + } + }, + "description": "None" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Tags" + ] + } + }, + "/api/v1/tag/get_objects/": { + "get": { + "parameters": [ + { + "in": "path", + "name": "tag_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "items": { + "$ref": "#/components/schemas/TaggedObjectEntityResponseSchema" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "List of tagged objects associated with a Tag" + }, + "302": { + "description": "Redirects to the current digest" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get all objects associated with a tag", + "tags": [ + "Tags" + ] + } + }, + "/api/v1/tag/related/{column_name}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "column_name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_related_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelatedResponseSchema" + } + } + }, + "description": "Related column data" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get related fields data", + "tags": [ + "Tags" + ] + } + }, + "/api/v1/tag/{object_type}/{object_id}/": { + "post": { + "description": "Adds tags to an object. Creates new tags if they do not already exist.", + "parameters": [ + { + "in": "path", + "name": "object_type", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "object_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "tags": { + "description": "list of tag names to add to object", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Tag schema", + "required": true + }, + "responses": { + "201": { + "description": "Tag added" + }, + "302": { + "description": "Redirects to the current digest" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Add tags to an object", + "tags": [ + "Tags" + ] + } + }, + "/api/v1/tag/{object_type}/{object_id}/{tag}/": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "tag", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "object_type", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "object_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Chart delete" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a tagged object", + "tags": [ + "Tags" + ] + } + }, + "/api/v1/tag/{pk}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item deleted" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Delete a tag", + "tags": [ + "Tags" + ] + }, + "get": { + "description": "Get an item model", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_item_schema" + } + } + }, + "in": "query", + "name": "q" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "description_columns": { + "properties": { + "column_name": { + "description": "The description for the column name. Will be translated by babel", + "example": "A Nice description for the column", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "description": "The item id", + "type": "string" + }, + "label_columns": { + "properties": { + "column_name": { + "description": "The label for the column name. Will be translated by babel", + "example": "A Nice label for the column", + "type": "string" + } + }, + "type": "object" + }, + "result": { + "$ref": "#/components/schemas/TagRestApi.get" + }, + "show_columns": { + "description": "A list of columns", + "items": { + "type": "string" + }, + "type": "array" + }, + "show_title": { + "description": "A title to render. Will be translated by babel", + "example": "Show Item Details", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Item from Model" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Get a tag detail information", + "tags": [ + "Tags" + ] + }, + "put": { + "description": "Changes a Tag.", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagRestApi.put" + } + } + }, + "description": "Chart schema", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/TagRestApi.put" + } + }, + "type": "object" + } + } + }, + "description": "Tag changed" + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "Update a tag", + "tags": [ + "Tags" + ] + } + }, + "/api/v1/tag/{pk}/favorites/": { + "delete": { + "description": "Remove the tag from the user favorite list", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Tag removed from favorites" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Tags" + ] + }, + "post": { + "description": "Marks the tag as favorite for the current user", + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Tag added to favorites" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "Tags" + ] + } + }, + "/api/v1/user/{user_id}/avatar.png": { + "get": { + "description": "Gets the avatar URL for the user with the given ID, or returns a 401 error if the user is unauthenticated.", + "parameters": [ + { + "description": "The ID of the user", + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "301": { + "description": "A redirect to the user's avatar URL" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "summary": "Get the user avatar", + "tags": [ + "User" + ] + } + }, + "/api/{version}/_openapi": { + "get": { + "description": "Get the OpenAPI spec for a specific API version", + "parameters": [ + { + "in": "path", + "name": "version", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "The OpenAPI spec" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "tags": [ + "OpenApi" + ] + } + } + }, + "servers": [ + { + "url": "http://localhost:8088" + } + ] +} From 8f6b44c67974fe65c1fc9bcbb471cf45f0a7f81a Mon Sep 17 00:00:00 2001 From: Volobuev Andrey Date: Mon, 6 Oct 2025 13:59:30 +0300 Subject: [PATCH 3/4] backup worked --- backup_script.py | 30 +- migration_script.py | 573 ++++++++++++++------- superset_tool/client.py | 624 ++++++++++++++--------- superset_tool/utils/logger.py | 215 ++++++-- superset_tool/utils/network.py | 5 +- superset_tool/utils/whiptail_fallback.py | 148 ++++++ whiptailtest.py | 29 ++ 7 files changed, 1144 insertions(+), 480 deletions(-) create mode 100644 superset_tool/utils/whiptail_fallback.py create mode 100644 whiptailtest.py diff --git a/backup_script.py b/backup_script.py index a8cb731..942a206 100644 --- a/backup_script.py +++ b/backup_script.py @@ -8,7 +8,7 @@ import logging import sys from pathlib import Path -from dataclasses import dataclass +from dataclasses import dataclass,field # [IMPORTS] Third-party from requests.exceptions import RequestException @@ -37,17 +37,18 @@ class BackupConfig: consolidate: bool = True rotate_archive: bool = True clean_folders: bool = True - retention_policy: RetentionPolicy = RetentionPolicy() + retention_policy: RetentionPolicy = field(default_factory=RetentionPolicy) # [ENTITY: Function('backup_dashboards')] # CONTRACT: -# PURPOSE: Выполняет бэкап всех доступных дашбордов для заданного клиента и окружения. +# PURPOSE: Выполняет бэкап всех доступных дашбордов для заданного клиента и окружения, пропуская ошибки экспорта. # PRECONDITIONS: # - `client` должен быть инициализированным экземпляром `SupersetClient`. # - `env_name` должен быть строкой, обозначающей окружение. # - `backup_root` должен быть валидным путем к корневой директории бэкапа. # POSTCONDITIONS: # - Дашборды экспортируются и сохраняются. +# - Ошибки экспорта логируются и не приводят к остановке скрипта. # - Возвращает `True` если все дашборды были экспортированы без критических ошибок, `False` иначе. def backup_dashboards( client: SupersetClient, @@ -90,7 +91,9 @@ 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}: {db_error}", exc_info=True) + logger.error(f"[STATE][backup_dashboards][FAILURE] Failed to export dashboard {dashboard_title} (ID: {dashboard_id}): {db_error}", exc_info=True) + # Продолжаем обработку других дашбордов + continue if config.consolidate: consolidate_archive_folders(backup_root / env_name , logger=logger) @@ -125,13 +128,18 @@ def main() -> int: backup_config = BackupConfig(rotate_archive=True) for env in environments: - results[env] = backup_dashboards( - clients[env], - env.upper(), - superset_backup_repo, - logger=logger, - config=backup_config - ) + try: + results[env] = backup_dashboards( + clients[env], + env.upper(), + superset_backup_repo, + logger=logger, + config=backup_config + ) + except Exception as env_error: + logger.critical(f"[STATE][main][FAILURE] Critical error for environment {env}: {env_error}", exc_info=True) + # Продолжаем обработку других окружений + results[env] = False if not all(results.values()): exit_code = 1 diff --git a/migration_script.py b/migration_script.py index 6ad8120..62059ff 100644 --- a/migration_script.py +++ b/migration_script.py @@ -1,237 +1,442 @@ -# -*- coding: utf-8 -*- -# CONTRACT: -# PURPOSE: Интерактивный скрипт для миграции ассетов Superset между различными окружениями. -# SPECIFICATION_LINK: mod_migration_script -# PRECONDITIONS: Наличие корректных конфигурационных файлов для подключения к Superset. -# POSTCONDITIONS: Выбранные ассеты успешно перенесены из исходного в целевое окружение. -# IMPORTS: [argparse, superset_tool.client, superset_tool.utils.init_clients, superset_tool.utils.logger, superset_tool.utils.fileio] -""" -[MODULE] Superset Migration Tool -@description: Интерактивный скрипт для миграции ассетов Superset между различными окружениями. -""" - -from whiptail import Whiptail +# [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 +# -------------------------------------------------------------- # [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.logger import SupersetLogger from superset_tool.utils.fileio import ( - save_and_unpack_dashboard, - read_dashboard_from_disk, + create_temp_file, # новый контекстный менеджер update_yamls, - create_dashboard_export + create_dashboard_export, +) +from superset_tool.utils.whiptail_fallback import ( + menu, + checklist, + yesno, + msgbox, + inputbox, + gauge, ) -# [ENTITY: Class('Migration')] -# CONTRACT: -# PURPOSE: Инкапсулирует логику и состояние процесса миграции. -# SPECIFICATION_LINK: class_migration -# ATTRIBUTES: -# - name: logger, type: SupersetLogger, description: Экземпляр логгера. -# - name: from_c, type: SupersetClient, description: Клиент для исходного окружения. -# - name: to_c, type: SupersetClient, description: Клиент для целевого окружения. -# - name: dashboards_to_migrate, type: list, description: Список дашбордов для миграции. -# - name: db_config_replacement, type: dict, description: Конфигурация для замены данных БД. +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/`` текущего рабочего каталога. +""" + class Migration: """ - Класс для управления процессом миграции дашбордов Superset. + :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). """ - def __init__(self): - self.logger = SupersetLogger(name="migration_script") - self.from_c: SupersetClient = None - self.to_c: SupersetClient = None - self.dashboards_to_migrate = [] - self.db_config_replacement = None - # END_FUNCTION___init__ - # [ENTITY: Function('run')] - # CONTRACT: - # PURPOSE: Запускает основной воркфлоу миграции, координируя все шаги. - # SPECIFICATION_LINK: func_run_migration - # PRECONDITIONS: None - # POSTCONDITIONS: Процесс миграции завершен. - def run(self): - """Запускает основной воркфлоу миграции.""" + # -------------------------------------------------------------- + # [ENTITY: Method('__init__')] + # -------------------------------------------------------------- + """ + :purpose: Создать сервис миграции и настроить логгер. + :preconditions: None. + :postconditions: ``self.logger`` готов к использованию; ``enable_delete_on_failure`` = ``False``. + """ + def __init__(self) -> None: + default_log_dir = Path.cwd() / "logs" + self.logger = SupersetLogger( + name="migration_script", + log_dir=default_log_dir, + level=logging.INFO, + console=True, + ) + self.enable_delete_on_failure = False + self.from_c: Optional[SupersetClient] = None + self.to_c: Optional[SupersetClient] = None + self.dashboards_to_migrate: List[dict] = [] + self.db_config_replacement: Optional[dict] = None + self._failed_imports: List[dict] = [] # <-- буфер ошибок + assert self.logger is not None, "Logger must be instantiated." + # [END_ENTITY] + + # -------------------------------------------------------------- + # [ENTITY: Method('run')] + # -------------------------------------------------------------- + """ + :purpose: Точка входа – последовательный запуск всех шагов миграции. + :preconditions: Логгер готов. + :postconditions: Скрипт завершён, пользователю выведено сообщение. + """ + def run(self) -> None: self.logger.info("[INFO][run][ENTER] Запуск скрипта миграции.") + 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_FUNCTION_run + self.logger.info("[INFO][run][EXIT] Скрипт миграции завершён.") + # [END_ENTITY] - # [ENTITY: Function('select_environments')] - # CONTRACT: - # PURPOSE: Шаг 1. Обеспечивает интерактивный выбор исходного и целевого окружений. - # SPECIFICATION_LINK: func_select_environments - # PRECONDITIONS: None - # POSTCONDITIONS: Атрибуты `self.from_c` и `self.to_c` инициализированы валидными клиентами Superset. - def select_environments(self): - """Шаг 1: Выбор окружений (источник и назначение).""" - self.logger.info("[INFO][select_environments][ENTER] Шаг 1/4: Выбор окружений.") - + # -------------------------------------------------------------- + # [ENTITY: Method('ask_delete_on_failure')] + # -------------------------------------------------------------- + """ + :purpose: Запросить у пользователя, следует ли удалять дашборд при ошибке импорта. + :preconditions: None. + :postconditions: ``self.enable_delete_on_failure`` установлен. + """ + 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", + self.enable_delete_on_failure, + ) + # [END_ENTITY] + + # -------------------------------------------------------------- + # [ENTITY: Method('select_environments')] + # -------------------------------------------------------------- + """ + :purpose: Выбрать исходное и целевое окружения Superset. + :preconditions: ``setup_clients`` успешно инициализирует все клиенты. + :postconditions: ``self.from_c`` и ``self.to_c`` установлены. + """ + def select_environments(self) -> None: + self.logger.info("[INFO][select_environments][ENTER] Шаг 1/5: Выбор окружений.") try: all_clients = setup_clients(self.logger) available_envs = list(all_clients.keys()) except Exception as e: - self.logger.error(f"[ERROR][select_environments][FAILURE] Ошибка при инициализации клиентов: {e}", exc_info=True) - w = Whiptail(title="Ошибка", backtitle="Superset Migration Tool") - w.msgbox("Не удалось инициализировать клиенты. Проверьте конфигурацию.") + self.logger.error("[ERROR][select_environments] %s", e, exc_info=True) + msgbox("Ошибка", "Не удалось инициализировать клиенты.") return - w = Whiptail(title="Выбор окружения", backtitle="Superset Migration Tool") - - # Select source environment - (return_code, from_env_name) = w.menu("Выберите исходное окружение:", available_envs) - if return_code == 0: - self.from_c = all_clients[from_env_name] - self.logger.info(f"[INFO][select_environments][STATE] Исходное окружение: {from_env_name}") - else: + rc, from_env_name = menu( + title="Выбор окружения", + prompt="Исходное окружение:", + choices=available_envs, + ) + if rc != 0: return + self.from_c = all_clients[from_env_name] + self.logger.info("[INFO][select_environments] from = %s", from_env_name) - # Select target environment available_envs.remove(from_env_name) - (return_code, to_env_name) = w.menu("Выберите целевое окружение:", available_envs) - if return_code == 0: - self.to_c = all_clients[to_env_name] - self.logger.info(f"[INFO][select_environments][STATE] Целевое окружение: {to_env_name}") - else: + rc, to_env_name = menu( + title="Выбор окружения", + prompt="Целевое окружение:", + choices=available_envs, + ) + if rc != 0: return - - self.logger.info("[INFO][select_environments][EXIT] Шаг 1 завершен.") - # END_FUNCTION_select_environments - - # [ENTITY: Function('select_dashboards')] - # CONTRACT: - # PURPOSE: Шаг 2. Обеспечивает интерактивный выбор дашбордов для миграции. - # SPECIFICATION_LINK: func_select_dashboards - # PRECONDITIONS: `self.from_c` должен быть инициализирован. - # POSTCONDITIONS: `self.dashboards_to_migrate` содержит список выбранных дашбордов. - def select_dashboards(self): - """Шаг 2: Выбор дашбордов для миграции.""" - self.logger.info("[INFO][select_dashboards][ENTER] Шаг 2/4: Выбор дашбордов.") + 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] + # -------------------------------------------------------------- + # [ENTITY: Method('select_dashboards')] + # -------------------------------------------------------------- + """ + :purpose: Позволить пользователю выбрать набор дашбордов для миграции. + :preconditions: ``self.from_c`` инициализирован. + :postconditions: ``self.dashboards_to_migrate`` заполнен. + """ + def select_dashboards(self) -> None: + self.logger.info("[INFO][select_dashboards][ENTER] Шаг 2/5: Выбор дашбордов.") try: - _, all_dashboards = self.from_c.get_dashboards() + _, all_dashboards = self.from_c.get_dashboards() # type: ignore[attr-defined] if not all_dashboards: - self.logger.warning("[WARN][select_dashboards][STATE] В исходном окружении не найдено дашбордов.") - w = Whiptail(title="Информация", backtitle="Superset Migration Tool") - w.msgbox("В исходном окружении не найдено дашбордов.") + self.logger.warning("[WARN][select_dashboards] No dashboards.") + msgbox("Информация", "В исходном окружении нет дашбордов.") return - w = Whiptail(title="Выбор дашбордов", backtitle="Superset Migration Tool") - - dashboard_options = [(str(d['id']), d['dashboard_title']) for d in all_dashboards] - - (return_code, selected_ids) = w.checklist("Выберите дашборды для миграции:", dashboard_options) + options = [("ALL", "Все дашборды")] + [ + (str(d["id"]), d["dashboard_title"]) for d in all_dashboards + ] - if return_code == 0: - self.dashboards_to_migrate = [d for d in all_dashboards if str(d['id']) in selected_ids] - self.logger.info(f"[INFO][select_dashboards][STATE] Выбрано дашбордов: {len(self.dashboards_to_migrate)}") + rc, selected = checklist( + title="Выбор дашбордов", + prompt="Отметьте нужные дашборды (введите номера):", + options=options, + ) + if rc != 0: + return + 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 + ] + self.logger.info( + "[INFO][select_dashboards] Выбрано %d дашбордов.", + len(self.dashboards_to_migrate), + ) except Exception as e: - self.logger.error(f"[ERROR][select_dashboards][FAILURE] Ошибка при получении или выборе дашбордов: {e}", exc_info=True) - w = Whiptail(title="Ошибка", backtitle="Superset Migration Tool") - w.msgbox("Произошла ошибка при работе с дашбордами.") - - self.logger.info("[INFO][select_dashboards][EXIT] Шаг 2 завершен.") - # END_FUNCTION_select_dashboards + self.logger.error("[ERROR][select_dashboards] %s", e, exc_info=True) + msgbox("Ошибка", "Не удалось получить список дашбордов.") + self.logger.info("[INFO][select_dashboards][EXIT] Шаг 2 завершён.") + # [END_ENTITY] - # [ENTITY: Function('confirm_db_config_replacement')] - # CONTRACT: - # PURPOSE: Шаг 3. Управляет процессом подтверждения и настройки замены конфигураций БД. - # SPECIFICATION_LINK: func_confirm_db_config_replacement - # PRECONDITIONS: `self.from_c` и `self.to_c` инициализированы. - # POSTCONDITIONS: `self.db_config_replacement` содержит конфигурацию для замены или `None`. - def confirm_db_config_replacement(self): - """Шаг 3: Подтверждение и настройка замены конфигурации БД.""" - self.logger.info("[INFO][confirm_db_config_replacement][ENTER] Шаг 3/4: Замена конфигурации БД.") - - w = Whiptail(title="Замена конфигурации БД", backtitle="Superset Migration Tool") - if w.yesno("Хотите ли вы заменить конфигурации баз данных в YAML-файлах?"): - (return_code, old_db_name) = w.inputbox("Введите имя заменяемой базы данных (например, db_dev):") - if return_code != 0: + # -------------------------------------------------------------- + # [ENTITY: Method('confirm_db_config_replacement')] + # -------------------------------------------------------------- + """ + :purpose: Запросить у пользователя, требуется ли заменить имена БД в YAML‑файлах. + :preconditions: None. + :postconditions: ``self.db_config_replacement`` либо ``None``, либо заполнен. + """ + def confirm_db_config_replacement(self) -> None: + if yesno("Замена БД", "Заменить конфигурацию БД в YAML‑файлах?"): + rc, old_name = inputbox("Замена БД", "Старое имя БД (например, db_dev):") + if rc != 0: return - - (return_code, new_db_name) = w.inputbox("Введите новое имя базы данных (например, db_prod):") - if return_code != 0: + rc, new_name = inputbox("Замена БД", "Новое имя БД (например, db_prod):") + if rc != 0: return - - self.db_config_replacement = {"old": {"database_name": old_db_name}, "new": {"database_name": new_db_name}} - self.logger.info(f"[INFO][confirm_db_config_replacement][STATE] Установлена ручная замена: {self.db_config_replacement}") + 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, + ) else: - self.logger.info("[INFO][confirm_db_config_replacement][STATE] Замена конфигурации БД пропущена.") + self.logger.info("[INFO][confirm_db_config_replacement] Skipped.") + # [END_ENTITY] - self.logger.info("[INFO][confirm_db_config_replacement][EXIT] Шаг 3 завершен.") - # END_FUNCTION_confirm_db_config_replacement - - # [ENTITY: Function('execute_migration')] - # CONTRACT: - # PURPOSE: Шаг 4. Выполняет фактическую миграцию выбранных дашбордов. - # SPECIFICATION_LINK: func_execute_migration - # PRECONDITIONS: Все предыдущие шаги (`select_environments`, `select_dashboards`) успешно выполнены. - # POSTCONDITIONS: Выбранные дашборды перенесены в целевое окружение. - def execute_migration(self): - """Шаг 4: Выполнение миграции и обновления конфигураций.""" - self.logger.info("[INFO][execute_migration][ENTER] Шаг 4/4: Выполнение миграции.") - w = Whiptail(title="Выполнение миграции", backtitle="Superset Migration Tool") - - if not self.dashboards_to_migrate: - self.logger.warning("[WARN][execute_migration][STATE] Нет дашбордов для миграции.") - w.msgbox("Нет дашбордов для миграции. Завершение.") + # -------------------------------------------------------------- + # [ENTITY: Method('_batch_delete_by_ids')] + # -------------------------------------------------------------- + """ + :purpose: Удалить набор дашбордов по их ID единым запросом. + :preconditions: + - ``ids`` – непустой список целых чисел. + :postconditions: Все указанные дашборды удалены (если они существовали). + :sideeffect: Делает HTTP‑запрос ``DELETE /dashboard/?q=[ids]``. + """ + 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.") return - total_dashboards = len(self.dashboards_to_migrate) - self.logger.info(f"[INFO][execute_migration][STATE] Начало миграции {total_dashboards} дашбордов.") - with w.gauge("Выполняется миграция...", width=60, height=10) as gauge: - for i, dashboard in enumerate(self.dashboards_to_migrate): + self.logger.info("[INFO][_batch_delete_by_ids] Deleting dashboards IDs: %s", ids) + # Формируем параметр q в виде JSON‑массива, как требует Superset. + q_param = json.dumps(ids) + response = self.to_c.network.request( + method="DELETE", + endpoint="/dashboard/", + params={"q": q_param}, + ) + # Superset обычно отвечает 200/204; проверяем поле ``result`` при наличии. + if isinstance(response, dict) and response.get("result", True) is False: + self.logger.warning("[WARN][_batch_delete_by_ids] Unexpected delete response: %s", response) + else: + self.logger.info("[INFO][_batch_delete_by_ids] Delete request completed.") + # [END_ENTITY] + + # -------------------------------------------------------------- + # [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`` производится + батч‑удаление и повторный импорт. + """ + def execute_migration(self) -> None: + if not self.dashboards_to_migrate: + self.logger.warning("[WARN][execute_migration] 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.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) + g.set_text(f"Миграция: {title} ({i + 1}/{total})") + g.set_percent(progress) + try: - dashboard_id = dashboard['id'] - dashboard_title = dashboard['dashboard_title'] - - progress = int((i / total_dashboards) * 100) - self.logger.debug(f"[DEBUG][execute_migration][PROGRESS] {progress}% - Миграция: {dashboard_title}") - gauge.set_text(f"Миграция: {dashboard_title} ({i+1}/{total_dashboards})") - gauge.set_percent(progress) + # ------------------- Экспорт ------------------- + exported_content, _ = self.from_c.export_dashboard(dash_id) # type: ignore[attr-defined] - self.logger.info(f"[INFO][execute_migration][PROGRESS] Миграция дашборда: {dashboard_title} (ID: {dashboard_id})") + # ------------------- Временный 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) - # 1. Экспорт - exported_content, _ = self.from_c.export_dashboard(dashboard_id) - zip_path, unpacked_path = save_and_unpack_dashboard(exported_content, f"temp_export_{dashboard_id}", unpack=True) - self.logger.info(f"[INFO][execute_migration][STATE] Дашборд экспортирован и распакован в {unpacked_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) - # 2. Обновление YAML, если нужно - if self.db_config_replacement: - update_yamls(db_configs=[self.db_config_replacement], path=str(unpacked_path)) - self.logger.info(f"[INFO][execute_migration][STATE] YAML-файлы обновлены.") + 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) - # 3. Упаковка и импорт - new_zip_path = f"migrated_dashboard_{dashboard_id}.zip" - create_dashboard_export(new_zip_path, [str(unpacked_path)]) - - self.to_c.import_dashboard(new_zip_path) - self.logger.info(f"[INFO][execute_migration][SUCCESS] Дашборд {dashboard_title} успешно импортирован.") + # ------------------- 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.") - except Exception as e: - self.logger.error(f"[ERROR][execute_migration][FAILURE] Ошибка при миграции дашборда {dashboard_title}: {e}", exc_info=True) - error_msg = f"Не удалось смигрировать дашборд: {dashboard_title}.\n\nОшибка: {e}" - w.msgbox(error_msg, width=60, height=15) - - gauge.set_percent(100) + # ------------------- Сборка нового 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.logger.info("[INFO][execute_migration][STATE] Миграция завершена.") - w.msgbox("Миграция завершена!", width=40, height=8) - self.logger.info("[INFO][execute_migration][EXIT] Шаг 4 завершен.") - # END_FUNCTION_execute_migration + # ------------------- Импорт ------------------- + self.to_c.import_dashboard( + file_name=tmp_new_zip, + dash_id=dash_id, + dash_slug=dash_slug, + ) # type: ignore[attr-defined] -# END_CLASS_Migration + # Если импорт прошёл без исключений – фиксируем успех + self.logger.info("[INFO][execute_migration][SUCCESS] Dashboard %s imported.", title) -# [MAIN_EXECUTION_BLOCK] + 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, + } + ) + msgbox("Ошибка", f"Не удалось мигрировать дашборд {title}.\n\n{exc}") + + 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._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"] + + # Один раз создаём временный 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.") + msgbox("Информация", "Миграция завершена!") + # [END_ENTITY] + +# [END_ENTITY: Service('Migration')] + +# -------------------------------------------------------------- +# Точка входа +# -------------------------------------------------------------- if __name__ == "__main__": - migration = Migration() - migration.run() -# END_MAIN_EXECUTION_BLOCK - -# END_MODULE_migration_script \ No newline at end of file + Migration().run() +# [END_FILE migration_script.py] +# -------------------------------------------------------------- \ No newline at end of file diff --git a/superset_tool/client.py b/superset_tool/client.py index c034be4..4002edc 100644 --- a/superset_tool/client.py +++ b/superset_tool/client.py @@ -1,82 +1,106 @@ -# pylint: disable=too-many-arguments,too-many-locals,too-many-statements,too-many-branches,unused-argument -""" -[MODULE] Superset API Client -@contract: Реализует полное взаимодействие с Superset API -""" +# [MODULE_PATH] superset_tool.client +# [FILE] client.py +# [SEMANTICS] superset, api, client, logging, error-handling, slug-support -# [IMPORTS] Стандартная библиотека +# -------------------------------------------------------------- +# [IMPORTS] +# -------------------------------------------------------------- import json -from typing import Optional, Dict, Tuple, List, Any, Union -import datetime -from pathlib import Path import zipfile +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union + from requests import Response -# [IMPORTS] Локальные модули from superset_tool.models import SupersetConfig -from superset_tool.exceptions import ( - ExportError, - InvalidZipFormatError -) +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] -# [CONSTANTS] -DEFAULT_TIMEOUT = 30 - -# [TYPE-ALIASES] -JsonType = Union[Dict[str, Any], List[Dict[str, Any]]] -ResponseType = Tuple[bytes, str] - +# -------------------------------------------------------------- +# [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` при передаче неверного типа конфигурации. +""" class SupersetClient: - """[MAIN-CONTRACT] Клиент для работы с Superset API""" - # [ENTITY: Function('__init__')] - # CONTRACT: - # PURPOSE: Инициализация клиента Superset. - # PRECONDITIONS: `config` должен быть валидным `SupersetConfig`. - # POSTCONDITIONS: Клиент успешно инициализирован. + """ + :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): self.logger = logger or SupersetLogger(name="SupersetClient") - self.logger.info("[INFO][SupersetClient.__init__][ENTER] Initializing SupersetClient.") + self.logger.info("[INFO][SupersetClient.__init__] Initializing SupersetClient.") self._validate_config(config) self.config = config - self.env = config.env self.network = APIClient( config=config.dict(), verify_ssl=config.verify_ssl, timeout=config.timeout, - logger=self.logger + logger=self.logger, ) - self.logger.info("[INFO][SupersetClient.__init__][SUCCESS] SupersetClient initialized successfully.") - # END_FUNCTION___init__ + self.delete_before_reimport: bool = False + self.logger.info("[INFO][SupersetClient.__init__] SupersetClient initialized.") + # [END_ENTITY] - # [ENTITY: Function('_validate_config')] - # CONTRACT: - # PURPOSE: Валидация конфигурации клиента. - # PRECONDITIONS: `config` должен быть экземпляром `SupersetConfig`. - # POSTCONDITIONS: Конфигурация валидна. + # -------------------------------------------------------------- + # [ENTITY: Method('_validate_config')] + # -------------------------------------------------------------- + """ + :purpose: Проверить, что передан объект :class:`SupersetConfig`. + :preconditions: ``config`` – произвольный объект. + :postconditions: При несовпадении типов возбуждается :class:`TypeError`. + """ def _validate_config(self, config: SupersetConfig) -> None: - self.logger.debug("[DEBUG][SupersetClient._validate_config][ENTER] Validating config.") + self.logger.debug("[DEBUG][_validate_config][ENTER] Validating SupersetConfig.") if not isinstance(config, SupersetConfig): - self.logger.error("[ERROR][SupersetClient._validate_config][FAILURE] Invalid config type.") + self.logger.error("[ERROR][_validate_config][FAILURE] Invalid config type.") raise TypeError("Конфигурация должна быть экземпляром SupersetConfig") - self.logger.debug("[DEBUG][SupersetClient._validate_config][SUCCESS] Config validated.") - # END_FUNCTION__validate_config + self.logger.debug("[DEBUG][_validate_config][SUCCESS] Config is valid.") + # [END_ENTITY] + # -------------------------------------------------------------- + # [ENTITY: Property('headers')] + # -------------------------------------------------------------- @property def headers(self) -> dict: - """[INTERFACE] Базовые заголовки для API-вызовов.""" + """Базовые HTTP‑заголовки, используемые клиентом.""" return self.network.headers - # END_FUNCTION_headers + # [END_ENTITY] - # [ENTITY: Function('get_dashboards')] - # CONTRACT: - # PURPOSE: Получение списка дашбордов с пагинацией. - # PRECONDITIONS: None - # POSTCONDITIONS: Возвращает кортеж с общим количеством и списком дашбордов. + # -------------------------------------------------------------- + # [ENTITY: Method('get_dashboards')] + # -------------------------------------------------------------- + """ + :purpose: Получить список дашбордов с поддержкой пагинации. + :preconditions: None. + :postconditions: Возвращается кортеж ``(total_count, list_of_dashboards)``. + """ def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: - self.logger.info("[INFO][SupersetClient.get_dashboards][ENTER] Getting dashboards.") + self.logger.info("[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( @@ -85,236 +109,368 @@ class SupersetClient: "base_query": validated_query, "total_count": total_count, "results_field": "result", - } + }, ) - self.logger.info("[INFO][SupersetClient.get_dashboards][SUCCESS] Got dashboards.") + self.logger.info("[INFO][get_dashboards][SUCCESS] Got dashboards.") return total_count, paginated_data - # END_FUNCTION_get_dashboards + # [END_ENTITY] - # [ENTITY: Function('get_dashboard')] - # CONTRACT: - # PURPOSE: Получение метаданных дашборда по ID или SLUG. - # PRECONDITIONS: `dashboard_id_or_slug` должен существовать. - # POSTCONDITIONS: Возвращает метаданные дашборда. - def get_dashboard(self, dashboard_id_or_slug: str) -> dict: - self.logger.info(f"[INFO][SupersetClient.get_dashboard][ENTER] Getting dashboard: {dashboard_id_or_slug}") - response_data = self.network.request( - method="GET", - endpoint=f"/dashboard/{dashboard_id_or_slug}", - ) - self.logger.info(f"[INFO][SupersetClient.get_dashboard][SUCCESS] Got dashboard: {dashboard_id_or_slug}") - return response_data.get("result", {}) - # END_FUNCTION_get_dashboard - - # [ENTITY: Function('get_datasets')] - # CONTRACT: - # PURPOSE: Получение списка датасетов с пагинацией. - # PRECONDITIONS: None - # POSTCONDITIONS: Возвращает кортеж с общим количеством и списком датасетов. - def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: - self.logger.info("[INFO][SupersetClient.get_datasets][ENTER] Getting datasets.") - total_count = self._fetch_total_object_count(endpoint="/dataset/") - base_query = { - "columns": ["id", "table_name", "sql", "database", "schema"], - "page": 0, - "page_size": 100 - } - validated_query = {**base_query, **(query or {})} - datasets = self._fetch_all_pages( - endpoint="/dataset/", - pagination_options={ - "base_query": validated_query, - "total_count": total_count, - "results_field": "result", - } - ) - self.logger.info("[INFO][SupersetClient.get_datasets][SUCCESS] Got datasets.") - return total_count, datasets - # END_FUNCTION_get_datasets - - # [ENTITY: Function('get_dataset')] - # CONTRACT: - # PURPOSE: Получение метаданных датасета по ID. - # PRECONDITIONS: `dataset_id` должен существовать. - # POSTCONDITIONS: Возвращает метаданные датасета. - def get_dataset(self, dataset_id: str) -> dict: - self.logger.info(f"[INFO][SupersetClient.get_dataset][ENTER] Getting dataset: {dataset_id}") - response_data = self.network.request( - method="GET", - endpoint=f"/dataset/{dataset_id}", - ) - self.logger.info(f"[INFO][SupersetClient.get_dataset][SUCCESS] Got dataset: {dataset_id}") - return response_data.get("result", {}) - # END_FUNCTION_get_dataset - - def get_databases(self) -> List[Dict]: - self.logger.info("[INFO][SupersetClient.get_databases][ENTER] Getting databases.") - response = self.network.request("GET", "/database/") - self.logger.info("[INFO][SupersetClient.get_databases][SUCCESS] Got databases.") - return response.get('result', []) - - # [ENTITY: Function('export_dashboard')] - # CONTRACT: - # PURPOSE: Экспорт дашборда в ZIP-архив. - # PRECONDITIONS: `dashboard_id` должен существовать. - # POSTCONDITIONS: Возвращает содержимое ZIP-архива и имя файла. + # -------------------------------------------------------------- + # [ENTITY: Method('export_dashboard')] + # -------------------------------------------------------------- + """ + :purpose: Скачать дашборд в виде ZIP‑архива. + :preconditions: ``dashboard_id`` – существующий идентификатор. + :postconditions: Возвращается бинарное содержимое и имя файла. + """ def export_dashboard(self, dashboard_id: int) -> Tuple[bytes, str]: - self.logger.info(f"[INFO][SupersetClient.export_dashboard][ENTER] Exporting dashboard: {dashboard_id}") + self.logger.info("[INFO][export_dashboard][ENTER] Exporting dashboard %s.", dashboard_id) response = self.network.request( method="GET", endpoint="/dashboard/export/", params={"q": json.dumps([dashboard_id])}, stream=True, - raw_response=True + raw_response=True, ) self._validate_export_response(response, dashboard_id) filename = self._resolve_export_filename(response, dashboard_id) - content = response.content - self.logger.info(f"[INFO][SupersetClient.export_dashboard][SUCCESS] Exported dashboard: {dashboard_id}") - return content, filename - # END_FUNCTION_export_dashboard + self.logger.info("[INFO][export_dashboard][SUCCESS] Exported dashboard %s.", dashboard_id) + return response.content, filename + # [END_ENTITY] - # [ENTITY: Function('_validate_export_response')] - # CONTRACT: - # PURPOSE: Валидация ответа экспорта. - # PRECONDITIONS: `response` должен быть валидным HTTP-ответом. - # POSTCONDITIONS: Ответ валиден. - def _validate_export_response(self, response: Response, dashboard_id: int) -> None: - self.logger.debug(f"[DEBUG][SupersetClient._validate_export_response][ENTER] Validating export response for dashboard: {dashboard_id}") - content_type = response.headers.get('Content-Type', '') - if 'application/zip' not in content_type: - self.logger.error(f"[ERROR][SupersetClient._validate_export_response][FAILURE] Invalid content type: {content_type}") - raise ExportError(f"Получен не ZIP-архив (Content-Type: {content_type})") - if not response.content: - self.logger.error("[ERROR][SupersetClient._validate_export_response][FAILURE] Empty response content.") - raise ExportError("Получены пустые данные при экспорте") - self.logger.debug(f"[DEBUG][SupersetClient._validate_export_response][SUCCESS] Export response validated for dashboard: {dashboard_id}") - # END_FUNCTION__validate_export_response + # -------------------------------------------------------------- + # [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 + self._validate_import_file(file_path) - # [ENTITY: Function('_resolve_export_filename')] - # CONTRACT: - # PURPOSE: Определение имени экспортируемого файла. - # PRECONDITIONS: `response` должен быть валидным HTTP-ответом. - # POSTCONDITIONS: Возвращает имя файла. - def _resolve_export_filename(self, response: Response, dashboard_id: int) -> str: - self.logger.debug(f"[DEBUG][SupersetClient._resolve_export_filename][ENTER] Resolving export filename for dashboard: {dashboard_id}") - filename = get_filename_from_headers(response.headers) - if not filename: - timestamp = datetime.datetime.now().strftime('%Y%m%dT%H%M%S') - filename = f"dashboard_export_{dashboard_id}_{timestamp}.zip" - self.logger.warning(f"[WARNING][SupersetClient._resolve_export_filename][STATE_CHANGE] Could not resolve filename from headers, generated: {filename}") - self.logger.debug(f"[DEBUG][SupersetClient._resolve_export_filename][SUCCESS] Resolved export filename: {filename}") - return filename - # END_FUNCTION__resolve_export_filename + try: + import_response = self._do_import(file_path) + self.logger.info("[INFO][import_dashboard] Imported %s.", file_path) + return import_response - # [ENTITY: Function('export_to_file')] - # CONTRACT: - # PURPOSE: Экспорт дашборда напрямую в файл. - # PRECONDITIONS: `output_dir` должен существовать. - # POSTCONDITIONS: Дашборд сохранен в файл. - def export_to_file(self, dashboard_id: int, output_dir: Union[str, Path]) -> Path: - self.logger.info(f"[INFO][SupersetClient.export_to_file][ENTER] Exporting dashboard {dashboard_id} to file in {output_dir}") - output_dir = Path(output_dir) - if not output_dir.exists(): - self.logger.error(f"[ERROR][SupersetClient.export_to_file][FAILURE] Output directory does not exist: {output_dir}") - raise FileNotFoundError(f"Директория {output_dir} не найдена") - content, filename = self.export_dashboard(dashboard_id) - target_path = output_dir / filename - with open(target_path, 'wb') as f: - f.write(content) - self.logger.info(f"[INFO][SupersetClient.export_to_file][SUCCESS] Exported dashboard {dashboard_id} to {target_path}") - return target_path - # END_FUNCTION_export_to_file + except Exception as exc: + # ----------------------------------------------------------------- + # 2️⃣ Логируем первую неудачу, пытаемся удалить и повторить, + # только если включён флаг ``delete_before_reimport``. + # ----------------------------------------------------------------- + self.logger.error( + "[ERROR][import_dashboard] First import attempt failed: %s", + exc, + exc_info=True, + ) + if not self.delete_before_reimport: + raise - # [ENTITY: Function('import_dashboard')] - # CONTRACT: - # PURPOSE: Импорт дашборда из ZIP-архива. - # PRECONDITIONS: `file_name` должен быть валидным ZIP-файлом. - # POSTCONDITIONS: Возвращает ответ API. - def import_dashboard(self, file_name: Union[str, Path]) -> Dict: - self.logger.info(f"[INFO][SupersetClient.import_dashboard][ENTER] Importing dashboard from: {file_name}") - self._validate_import_file(file_name) - import_response = self.network.upload_file( + # ----------------------------------------------------------------- + # 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 – считаем невозможным корректно удалить. + if target_id is None: + self.logger.error("[ERROR][import_dashboard] No ID available for delete‑retry.") + raise + + # ----------------------------------------------------------------- + # 4️⃣ Удаляем найденный дашборд (по ID) + # ----------------------------------------------------------------- + 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 + + # ----------------------------------------------------------------- + # 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. + """ + 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", }, - extra_data={'overwrite': 'true'}, - timeout=self.config.timeout * 2 + extra_data={"overwrite": "true"}, + timeout=self.config.timeout * 2, ) - self.logger.info(f"[INFO][SupersetClient.import_dashboard][SUCCESS] Imported dashboard from: {file_name}") - return import_response - # END_FUNCTION_import_dashboard + # [END_ENTITY] - # [ENTITY: Function('_validate_query_params')] - # CONTRACT: - # PURPOSE: Нормализация и валидация параметров запроса. - # PRECONDITIONS: None - # POSTCONDITIONS: Возвращает валидный словарь параметров. + # -------------------------------------------------------------- + # [ENTITY: Method('delete_dashboard')] + # -------------------------------------------------------------- + """ + :purpose: Удалить дашборд **по ID или slug**. + :preconditions: + - ``dashboard_id`` – int ID **или** str slug дашборда. + :postconditions: На уровне API считается, что ресурс удалён + (HTTP 200/204). Логируется результат операции. + """ + 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`` – проверяем. + if response.get("result", True) is not False: + self.logger.info("[INFO][delete_dashboard] Dashboard %s deleted.", dashboard_id) + else: + self.logger.warning("[WARN][delete_dashboard] Unexpected response while deleting %s.", dashboard_id) + # [END_ENTITY] + + # -------------------------------------------------------------- + # [ENTITY: Method('_extract_dashboard_id_from_zip')] + # -------------------------------------------------------------- + """ + :purpose: Попытаться извлечь **ID** дашборда из ``metadata.yaml`` внутри ZIP‑архива. + :preconditions: ``file_name`` – путь к корректному ZIP‑файлу. + :postconditions: Возвращается ``int`` ID или ``None``. + """ + def _extract_dashboard_id_from_zip(self, file_name: Union[str, Path]) -> Optional[int]: + try: + import yaml + with zipfile.ZipFile(file_name, "r") as zf: + for name in zf.namelist(): + if name.endswith("metadata.yaml"): + with zf.open(name) as meta_file: + meta = yaml.safe_load(meta_file.read()) + dash_id = meta.get("dashboard_uuid") or meta.get("dashboard_id") + if dash_id is not None: + return int(dash_id) + except Exception as exc: + self.logger.error("[ERROR][_extract_dashboard_id_from_zip] %s", exc, exc_info=True) + return None + # [END_ENTITY] + + # -------------------------------------------------------------- + # [ENTITY: Method('_extract_dashboard_slug_from_zip')] + # -------------------------------------------------------------- + """ + :purpose: Попытаться извлечь **slug** дашборда из ``metadata.yaml`` внутри ZIP‑архива. + :preconditions: ``file_name`` – путь к корректному ZIP‑файлу. + :postconditions: Возвращается строка‑slug или ``None``. + """ + def _extract_dashboard_slug_from_zip(self, file_name: Union[str, Path]) -> Optional[str]: + try: + import yaml + with zipfile.ZipFile(file_name, "r") as zf: + for name in zf.namelist(): + if name.endswith("metadata.yaml"): + with zf.open(name) as meta_file: + meta = yaml.safe_load(meta_file.read()) + slug = meta.get("slug") + if slug: + return str(slug) + except Exception as exc: + self.logger.error("[ERROR][_extract_dashboard_slug_from_zip] %s", exc, exc_info=True) + return None + # [END_ENTITY] + + # -------------------------------------------------------------- + # [ENTITY: Method('_validate_export_response')] + # -------------------------------------------------------------- + """ + :purpose: Проверить, что ответ от ``/dashboard/export/`` – ZIP‑архив с данными. + :preconditions: ``response`` – объект :class:`requests.Response`. + :postconditions: При несоответствии возбуждается :class:`ExportError`. + """ + 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})") + 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: Возвращается строка‑имя файла. + """ + 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) + return filename + # [END_ENTITY] + + # -------------------------------------------------------------- + # [ENTITY: Method('_validate_query_params')] + # -------------------------------------------------------------- + """ + :purpose: Сформировать корректный набор параметров запроса. + :preconditions: ``query`` – любой словарь или ``None``. + :postconditions: Возвращается словарь с обязательными полями. + """ def _validate_query_params(self, query: Optional[Dict]) -> Dict: - self.logger.debug("[DEBUG][SupersetClient._validate_query_params][ENTER] Validating query params.") base_query = { "columns": ["slug", "id", "changed_on_utc", "dashboard_title", "published"], "page": 0, - "page_size": 1000 + "page_size": 1000, } - validated_query = {**base_query, **(query or {})} - self.logger.debug(f"[DEBUG][SupersetClient._validate_query_params][SUCCESS] Validated query params: {validated_query}") - return validated_query - # END_FUNCTION__validate_query_params + validated = {**base_query, **(query or {})} + self.logger.debug("[DEBUG][_validate_query_params] %s", validated) + return validated + # [END_ENTITY] - # [ENTITY: Function('_fetch_total_object_count')] - # CONTRACT: - # PURPOSE: Получение общего количества объектов. - # PRECONDITIONS: `endpoint` должен быть валидным. - # POSTCONDITIONS: Возвращает общее количество объектов. - def _fetch_total_object_count(self, endpoint:str) -> int: - self.logger.debug(f"[DEBUG][SupersetClient._fetch_total_object_count][ENTER] Fetching total object count for endpoint: {endpoint}") - query_params_for_count = {'page': 0, 'page_size': 1} + # -------------------------------------------------------------- + # [ENTITY: Method('_fetch_total_object_count')] + # -------------------------------------------------------------- + """ + :purpose: Получить общее количество объектов по указанному endpoint. + :preconditions: ``endpoint`` – строка, начинающаяся с «/». + :postconditions: Возвращается целое число. + """ + def _fetch_total_object_count(self, endpoint: str) -> int: + query_params_for_count = {"page": 0, "page_size": 1} count = self.network.fetch_paginated_count( endpoint=endpoint, query_params=query_params_for_count, - count_field="count" + count_field="count", ) - self.logger.debug(f"[DEBUG][SupersetClient._fetch_total_object_count][SUCCESS] Fetched total object count: {count}") + self.logger.debug("[DEBUG][_fetch_total_object_count] %s → %s", endpoint, count) return count - # END_FUNCTION__fetch_total_object_count + # [END_ENTITY] - # [ENTITY: Function('_fetch_all_pages')] - # CONTRACT: - # PURPOSE: Обход всех страниц пагинированного API. - # PRECONDITIONS: `pagination_options` должен содержать необходимые параметры. - # POSTCONDITIONS: Возвращает список всех объектов. - def _fetch_all_pages(self, endpoint:str, pagination_options: Dict) -> List[Dict]: - self.logger.debug(f"[DEBUG][SupersetClient._fetch_all_pages][ENTER] Fetching all pages for endpoint: {endpoint}") + # -------------------------------------------------------------- + # [ENTITY: Method('_fetch_all_pages')] + # -------------------------------------------------------------- + """ + :purpose: Обойти все страницы пагинированного API. + :preconditions: ``pagination_options`` – словарь, сформированный + в ``_validate_query_params`` и ``_fetch_total_object_count``. + :postconditions: Возвращается список всех объектов. + """ + 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 + pagination_options=pagination_options, ) - self.logger.debug(f"[DEBUG][SupersetClient._fetch_all_pages][SUCCESS] Fetched all pages for endpoint: {endpoint}") + self.logger.debug("[DEBUG][_fetch_all_pages] Fetched %s items from %s.", len(all_data), endpoint) return all_data - # END_FUNCTION__fetch_all_pages + # [END_ENTITY] - # [ENTITY: Function('_validate_import_file')] - # CONTRACT: - # PURPOSE: Проверка файла перед импортом. - # PRECONDITIONS: `zip_path` должен быть путем к файлу. - # POSTCONDITIONS: Файл валиден. + # -------------------------------------------------------------- + # [ENTITY: Method('_validate_import_file')] + # -------------------------------------------------------------- + """ + :purpose: Проверить, что файл существует, является ZIP‑архивом и + содержит ``metadata.yaml``. + :preconditions: ``zip_path`` – путь к файлу. + :postconditions: При невалидном файле возбуждается :class:`InvalidZipFormatError`. + """ def _validate_import_file(self, zip_path: Union[str, Path]) -> None: - self.logger.debug(f"[DEBUG][SupersetClient._validate_import_file][ENTER] Validating import file: {zip_path}") path = Path(zip_path) if not path.exists(): - self.logger.error(f"[ERROR][SupersetClient._validate_import_file][FAILURE] Import file does not exist: {zip_path}") + 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(f"[ERROR][SupersetClient._validate_import_file][FAILURE] Import file is not a zip file: {zip_path}") - raise InvalidZipFormatError(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(f"[ERROR][SupersetClient._validate_import_file][FAILURE] Import file does not contain metadata.yaml: {zip_path}") + self.logger.error("[ERROR][_validate_import_file] Not a zip file: %s", zip_path) + raise InvalidZipFormatError(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(f"[DEBUG][SupersetClient._validate_import_file][SUCCESS] Validated import file: {zip_path}") - # END_FUNCTION__validate_import_file + 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)``. + """ + def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: + self.logger.info("[INFO][get_datasets][ENTER] Fetching datasets.") + validated_query = self._validate_query_params(query) + total_count = self._fetch_total_object_count(endpoint="/dataset/") + paginated_data = self._fetch_all_pages( + endpoint="/dataset/", + pagination_options={ + "base_query": validated_query, + "total_count": total_count, + "results_field": "result", + }, + ) + self.logger.info("[INFO][get_datasets][SUCCESS] Got datasets.") + return total_count, paginated_data + # [END_ENTITY] + + +# [END_FILE client.py] \ No newline at end of file diff --git a/superset_tool/utils/logger.py b/superset_tool/utils/logger.py index 59111f0..d0c44c4 100644 --- a/superset_tool/utils/logger.py +++ b/superset_tool/utils/logger.py @@ -1,88 +1,205 @@ -# [MODULE] Superset Tool Logger Utility -# PURPOSE: Предоставляет стандартизированный класс-обертку `SupersetLogger` для настройки и использования логирования в проекте. -# COHERENCE: Модуль согласован со стандартной библиотекой `logging`, расширяя ее для нужд проекта. +# [MODULE_PATH] superset_tool.utils.logger +# [FILE] logger.py +# [SEMANTICS] logging, utils, ai‑friendly, infrastructure +# -------------------------------------------------------------- +# [IMPORTS] +# -------------------------------------------------------------- import logging import sys from datetime import datetime from pathlib import Path -from typing import Optional +from typing import Optional, Any, Mapping +# [END_IMPORTS] -# CONTRACT: -# PURPOSE: Обеспечивает унифицированную настройку логгера с выводом в консоль и/или файл. -# PRECONDITIONS: -# - `name` должен быть строкой. -# - `level` должен быть валидным уровнем логирования (например, `logging.INFO`). -# POSTCONDITIONS: -# - Создает и настраивает логгер с указанным именем и уровнем. -# - Добавляет обработчики для вывода в файл (если указан `log_dir`) и в консоль (если `console=True`). -# - Очищает все предыдущие обработчики для данного логгера, чтобы избежать дублирования. -# PARAMETERS: -# - name: str - Имя логгера. -# - log_dir: Optional[Path] - Директория для сохранения лог-файлов. -# - level: int - Уровень логирования. -# - console: bool - Флаг для включения вывода в консоль. +# -------------------------------------------------------------- +# [ENTITY: Service('SupersetLogger')] +# -------------------------------------------------------------- +""" +:purpose: Универсальная обёртка над ``logging.Logger``. Позволяет: + • задавать уровень и вывод в консоль/файл, + • передавать произвольные ``extra``‑поля, + • использовать привычный API (info, debug, warning, error, + critical, exception) без «падения» при неверных аргументах. +:preconditions: + - ``name`` – строка‑идентификатор логгера, + - ``level`` – валидный уровень из ``logging``, + - ``log_dir`` – при указании директория, куда будет писаться файл‑лог. +:postconditions: + - Создан полностью сконфигурированный ``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 - ): + console: bool = True, + ) -> None: self.logger = logging.getLogger(name) self.logger.setLevel(level) + self.logger.propagate = False # ← не «прокидываем» записи выше - formatter = logging.Formatter( - '%(asctime)s - %(levelname)s - %(message)s' - ) + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") - # [ANCHOR] HANDLER_RESET - # Очищаем существующие обработчики, чтобы избежать дублирования вывода при повторной инициализации. + # ---- Очистка предыдущих обработчиков (важно при повторных инициализациях) ---- if self.logger.hasHandlers(): self.logger.handlers.clear() - # [ANCHOR] FILE_HANDLER + # ---- Файловый обработчик (если указана директория) ---- 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' + log_dir / f"{name}_{timestamp}.log", encoding="utf-8" ) file_handler.setFormatter(formatter) self.logger.addHandler(file_handler) - # [ANCHOR] CONSOLE_HANDLER + # ---- Консольный обработчик ---- if console: console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(formatter) self.logger.addHandler(console_handler) - # CONTRACT: - # PURPOSE: (HELPER) Генерирует строку с текущей датой для имени лог-файла. - # RETURN: str - Отформатированная дата (YYYYMMDD). - def _get_timestamp(self) -> str: - return datetime.now().strftime("%Y%m%d") - # END_FUNCTION__get_timestamp + # [END_ENTITY] - # [INTERFACE] Методы логирования - def info(self, message: str, extra: Optional[dict] = None, exc_info: bool = False): - self.logger.info(message, 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) - def error(self, message: str, extra: Optional[dict] = None, exc_info: bool = False): - self.logger.error(message, extra=extra, exc_info=exc_info) + # [END_ENTITY] - def warning(self, message: str, extra: Optional[dict] = None, exc_info: bool = False): - self.logger.warning(message, extra=extra, exc_info=exc_info) + # -------------------------------------------------------------- + # [ENTITY: Method('info')] + # -------------------------------------------------------------- + """ + :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] - def critical(self, message: str, extra: Optional[dict] = None, exc_info: bool = False): - self.logger.critical(message, extra=extra, exc_info=exc_info) + # -------------------------------------------------------------- + # [ENTITY: Method('debug')] + # -------------------------------------------------------------- + """ + :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] - def debug(self, message: str, extra: Optional[dict] = None, exc_info: bool = False): - self.logger.debug(message, extra=extra, exc_info=exc_info) + # -------------------------------------------------------------- + # [ENTITY: Method('warning')] + # -------------------------------------------------------------- + """ + :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] - def exception(self, message: str, *args, **kwargs): - self.logger.exception(message, *args, **kwargs) -# END_CLASS_SupersetLogger + # -------------------------------------------------------------- + # [ENTITY: Method('error')] + # -------------------------------------------------------------- + """ + :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] -# END_MODULE_logger + # -------------------------------------------------------------- + # [ENTITY: Method('critical')] + # -------------------------------------------------------------- + """ + :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``). + """ + 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 diff --git a/superset_tool/utils/network.py b/superset_tool/utils/network.py index 062e5fd..67bf32d 100644 --- a/superset_tool/utils/network.py +++ b/superset_tool/utils/network.py @@ -99,7 +99,7 @@ class APIClient: "csrf_token": csrf_token } self._authenticated = True - self.logger.info("[INFO][APIClient.authenticate][SUCCESS] Authenticated successfully.") + self.logger.info(f"[INFO][APIClient.authenticate][SUCCESS] Authenticated successfully. Tokens {self._tokens}") return self._tokens except requests.exceptions.HTTPError as e: self.logger.error(f"[ERROR][APIClient.authenticate][FAILURE] Authentication failed: {e}") @@ -132,12 +132,13 @@ class APIClient: _headers = self.headers.copy() if headers: _headers.update(headers) + timeout = kwargs.pop('timeout', self.request_settings.get("timeout", DEFAULT_TIMEOUT)) try: response = self.session.request( method, full_url, headers=_headers, - timeout=self.request_settings.get("timeout", DEFAULT_TIMEOUT), + timeout=timeout, **kwargs ) response.raise_for_status() diff --git a/superset_tool/utils/whiptail_fallback.py b/superset_tool/utils/whiptail_fallback.py new file mode 100644 index 0000000..d2ef135 --- /dev/null +++ b/superset_tool/utils/whiptail_fallback.py @@ -0,0 +1,148 @@ +# [MODULE_PATH] superset_tool.utils.whiptail_fallback +# [FILE] whiptail_fallback.py +# [SEMANTICS] ui, fallback, console, utils, non‑interactive + +# -------------------------------------------------------------- +# [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) + for idx, item in enumerate(choices, 1): + print(f"{idx}) {item}") + + try: + raw = input("\nВведите номер (0 – отмена): ").strip() + sel = int(raw) + if sel == 0: + return 1, None + return 0, choices[sel - 1] + except Exception: + return 1, None + + +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) + for idx, (val, label) in enumerate(options, 1): + print(f"{idx}) [{val}] {label}") + + raw = input("\nВведите номера через запятую (пустой ввод → отказ): ").strip() + 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: + return 1, [] + + +def yesno( + title: str, + question: str, + backtitle: str = "Superset Migration Tool", +) -> bool: + """True → пользователь ответил «да». """ + 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.""" + 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 → успешно.""" + print(f"\n=== {title} ===") + val = input(f"{prompt}\n") + if val == "": + return 1, None + return 0, val + + +# -------------------------------------------------------------- +# [ENTITY: Service('ConsoleGauge')] +# -------------------------------------------------------------- +""" +:purpose: Минимальная имитация ``whiptail``‑gauge в консоли. +""" + +class _ConsoleGauge: + """Контекст‑менеджер для простого прогресс‑бара.""" + def __init__(self, title: str, width: int = 60, height: int = 10): + 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() + + def set_text(self, txt: str) -> None: + 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] + +def gauge( + title: str, + width: int = 60, + height: int = 10, +) -> Any: + """Always returns the console fallback gauge.""" + return _ConsoleGauge(title, width, height) +# [END_ENTITY] + +# -------------------------------------------------------------- +# [END_FILE whiptail_fallback.py] +# -------------------------------------------------------------- \ No newline at end of file diff --git a/whiptailtest.py b/whiptailtest.py new file mode 100644 index 0000000..16280e4 --- /dev/null +++ b/whiptailtest.py @@ -0,0 +1,29 @@ +# test_whiptail.py +from superset_tool.utils.whiptail_fallback import ( + menu, checklist, yesno, msgbox, inputbox, gauge, +) + +rc, env = menu('Тестовое меню', 'Выберите среду:', ['dev', 'prod']) +print('menu →', rc, env) + +rc, ids = checklist( + 'Тестовый чек‑лист', + 'Выберите пункты:', + [('1', 'Первый'), ('2', 'Второй'), ('3', 'Третий')], +) +print('checklist →', rc, ids) + +if yesno('Вопрос', 'Продолжить?'): + print('Ответ – ДА') +else: + print('Ответ – НЕТ') + +rc, txt = inputbox('Ввод', 'Введите произвольный текст:') +print('inputbox →', rc, txt) + +msgbox('Сообщение', 'Это просто тестовое сообщение.') + +with gauge('Прогресс‑бар') as g: + for i in range(0, 101, 20): + g.set_text(f'Шаг {i // 20 + 1}') + g.set_percent(i) \ No newline at end of file From b550cb38ff1a045a18764b4c202402e3740e774b Mon Sep 17 00:00:00 2001 From: Volobuev Andrey Date: Mon, 6 Oct 2025 14:04:51 +0300 Subject: [PATCH 4/4] remove test scripts --- temp_pylint_runner.py | 7 ------- whiptailtest.py | 29 ----------------------------- 2 files changed, 36 deletions(-) delete mode 100644 temp_pylint_runner.py delete mode 100644 whiptailtest.py diff --git a/temp_pylint_runner.py b/temp_pylint_runner.py deleted file mode 100644 index e4e1c5c..0000000 --- a/temp_pylint_runner.py +++ /dev/null @@ -1,7 +0,0 @@ -import sys -import os -import pylint.lint - -sys.path.append(os.getcwd()) - -pylint.lint.Run(['superset_tool/utils/fileio.py']) \ No newline at end of file diff --git a/whiptailtest.py b/whiptailtest.py deleted file mode 100644 index 16280e4..0000000 --- a/whiptailtest.py +++ /dev/null @@ -1,29 +0,0 @@ -# test_whiptail.py -from superset_tool.utils.whiptail_fallback import ( - menu, checklist, yesno, msgbox, inputbox, gauge, -) - -rc, env = menu('Тестовое меню', 'Выберите среду:', ['dev', 'prod']) -print('menu →', rc, env) - -rc, ids = checklist( - 'Тестовый чек‑лист', - 'Выберите пункты:', - [('1', 'Первый'), ('2', 'Второй'), ('3', 'Третий')], -) -print('checklist →', rc, ids) - -if yesno('Вопрос', 'Продолжить?'): - print('Ответ – ДА') -else: - print('Ответ – НЕТ') - -rc, txt = inputbox('Ввод', 'Введите произвольный текст:') -print('inputbox →', rc, txt) - -msgbox('Сообщение', 'Это просто тестовое сообщение.') - -with gauge('Прогресс‑бар') as g: - for i in range(0, 101, 20): - g.set_text(f'Шаг {i // 20 + 1}') - g.set_percent(i) \ No newline at end of file