Python как источник данных виджета

Python как источник данных виджета

Python-скрипт можно использовать как источник данных виджета — вместо DAX-меры и измерения из модели. Скрипт возвращает таблицу, а виджет отрисовывает её штатными средствами (график, таблица, круговая диаграмма и т.д.).

Это позволяет:

  • получать данные из внешних систем по REST — для визуализации или для передачи данных во внешний сервис;

  • выполнять финальные преобразования и вычисления средствами библиотеки Pandas;

  • использовать виджет в кросс-фильтрации наравне со стандартными виджетами.

Python-режим — это не отдельный тип виджета. В этот режим можно перевести любой стандартный виджет: его источником данных становится скрипт.

Включение Python-режима

На панели «Виджеты» откройте раздел «Оформление» и включите «Python-режим». Режим доступен для всех стандартных виджетов Visiology.

Затем на вкладке «Виджеты» нажмите кнопку «Открыть редактор Python» — откроется область для работы с кодом.

Порядок настройки виджета

  1. Включите Python-режим и откройте редактор Python.

  2. Напишите скрипт. Результат поместите в DataFrame и верните его через return.

  3. Нажмите «Выполнить код» — в нижней части редактора появятся колонки результата.

  4. Переместите колонки результата в поля виджета (Ось X, Ось Y, Легенда, Значение и т.д.). Для мер выберите агрегацию (Sum, Min, Max, Count, Average, DistinctCount).

  5. Сохраните виджет.

Возврат результата

Чтобы вернуть результат, поместите его в DataFrame и укажите после return. Поддерживаются pandas.DataFrame, polars.DataFrame и pyarrow.Table.

import pandas as pd df = pd.DataFrame({"Категория": ["A", "B"], "Значение": [10, 20]}) return df

Результатом скрипта должна быть таблица.

Скрипт формирует только данные. Внешний вид виджета — тип диаграммы, цвета, серии, оси, форматирование — настраивается стандартными средствами виджета, а не из скрипта. Прямого доступа к объекту визуализации из Python нет.

Доступные библиотеки

В скрипте доступны перечисленные ниже библиотеки. Их нужно импортировать обычным import; устанавливать ничего не требуется.

Библиотека

Версия

Назначение

Библиотека

Версия

Назначение

pandas

2.2.3

таблицы; основной формат результата

polars

1.18.0

быстрые таблицы; альтернатива pandas

numpy

2.1.3

числовые вычисления

pyarrow

18.1.0

колоночные данные

requests

2.32.3

HTTP-запросы к внешним источникам

httpx

0.27.2

HTTP-запросы (синхронные и асинхронные)

Заблокированы модули subprocess, multiprocessing, threading, _thread, ctypes, cffi — при их импорте возникает понятная ошибка (запуск процессов, фоновые потоки и вызовы нативного кода в виджетах не поддерживаются).

Дополнительные библиотеки можно подключить через администратора платформы — см. раздел «Установка сторонних библиотек».

Получение данных из модели — client.query

Функция client.query запрашивает данные из модели Visiology. Группировку и агрегацию выполняет Formula Engine — с учётом политик доступа (RLS/OLS) и фильтров дашборда.

df = client.query( rows=["'Товары'[Категория]"], # поля-измерения по строкам columns=["'Период'[Год]"], # поля-измерения по столбцам (необязательно) measures={"'Продажи'[Сумма]": "sum"}, # пары «колонка: агрегация» ).to_pandas() return df
  • rows, columns — поля-измерения в формате 'Таблица'[Поле].

  • measures — пары «колонка: агрегация». Допустимые агрегации: sum, min, max, count, average, distinct_count.

  • filters — по умолчанию применяются все фильтры дашборда; при необходимости их можно переопределить (см. ниже).

Функция возвращает таблицу в формате pyarrow.Table. Чтобы работать с ней в Pandas, добавьте .to_pandas().

client.query рассчитан на агрегированные или срезанные данные. Число строк в его результате ограничено — тем же лимитом, что и выгрузка в Excel (значение задаётся в конфигурации платформы); при превышении запрос к Formula Engine не выполнится. Поэтому агрегируйте в самом запросе (задавайте агрегаты в measures), а не получайте сырые строки целиком. Подробнее — в разделе «Среда выполнения и ограничения».

Данные возвращаются с учётом прав текущего пользователя (RLS/OLS) — подробнее см. раздел «Авторизация и безопасность данных».

Контекст дашборда — context

Объект context содержит состояние дашборда на момент выполнения скрипта и доступен только для чтения:

context.filters # активные фильтры дашборда context.measures # меры виджета context.drill_enabled # включён ли drill-down context.drill_path # шаги drill-down context.take # ограничение на число строк context.dashboard_id, context.widget_id, context.workspace_id, context.dataset_id, context.user_id

Работа с фильтрами:

for f in context.filters: ... # перебрать фильтры context.filters.all() # список всех фильтров context.filters.get("Регион") # значение фильтра по колонке (или None) context.filters.get("Регион", []) # со значением по умолчанию

get(...) возвращает список выбранных значений (например, ["Москва", "СПб"]) для обычного фильтра или {"start": ..., "end": ...} для диапазона дат или чисел.

По умолчанию client.query применяет все фильтры дашборда. Это поведение можно изменить:

client.query(..., filters=[]) # без фильтров client.query(..., filters=context.filters.exclude("Год")) # все фильтры, кроме «Год» client.query(..., filters=context.filters.only("Регион")) # только «Регион»

Запросы к внешним системам (REST)

С помощью requests (или httpx) скрипт обращается к внешним сервисам — например, получает курсы валют, биржевые котировки или результат расчёта от стороннего сервиса:

import pandas as pd import requests response = requests.get("https://api.example.com/data", timeout=15) df = pd.DataFrame(response.json()) return df

Запрос можно построить на основе контекста: взять выбранные значения из context.filters и передать их во внешний сервис.

Платформа исходящие запросы не ограничивает — доступны любые внешние сервисы и HTTP-методы (GET, POST и т.д.). При этом фактическая доступность зависит от сетевой конфигурации стенда: в закрытом контуре внешний интернет может быть недоступен, а администратор может ограничить исходящий трафик (firewall, прокси). Отдельной сетевой изоляции и защиты от обращений к внутренним адресам (SSRF) нет — это учитывают при настройке безопасности платформы.

Рекомендации:

  • Параллельное выполнение запросов недоступно. Фоновые потоки и процессы заблокированы (threading, multiprocessing), поэтому несколько REST-запросов выполняются последовательно. Учитывайте это вместе с общим лимитом времени выполнения скрипта.

  • В каждом внешнем запросе следует задавать явный таймаут (параметр timeout). Иначе ожидание ответа может упереться в общий лимит времени выполнения скрипта (см. раздел «Среда выполнения и ограничения»).

  • Объём принимаемых данных следует ограничивать на стороне внешнего сервиса (фильтрация, агрегация) — в скрипт должны поступать уже подготовленные данные.

Авторизация во внешнем сервисе реализуется в коде скрипта — см. раздел «Авторизация и безопасность данных».

Режим обновления виджета

По умолчанию Python-виджет работает в автоматическом режиме: скрипт перезапускается сам при каждой смене фильтров дашборда и при выборе значения в других виджетах. Это удобно для лёгких вычислений по данным модели.

Если же скрипт делает ресурсоёмкий расчёт или обращается к внешнему сервису, постоянный перезапуск может быть нежелательным. В этом случае виджет можно перевести в ручной режим обновления.

Переключить режим можно в настройках виджета — тумблер «Ручное обновление».

В ручном режиме виджет не пересчитывается автоматически при смене фильтров. Скрипт выполняется только по нажатию кнопки обновления в правом верхнем углу виджета.

Так аналитик сам выбирает момент запуска — например, когда выставлены все нужные фильтры — и не нагружает внешний сервис на каждое изменение контекста.

Ручной режим уместен, когда:

  • скрипт выполняет дорогой запрос к внешнему сервису и запуск нужно контролировать вручную;

  • расчёт длительный или требует явного подтверждения (например, «Рассчитать прогноз»).

Кросс-фильтрация

Виджет на Python участвует в кросс-фильтрации наравне со стандартными:

  • входящая — виджет реагирует на фильтры и выбор значений в других виджетах;

  • исходящая — выбор значения в Python-виджете фильтрует другие виджеты.

Чтобы работала исходящая кросс-фильтрация, свяжите колонку результата с полем модели: переместите поле модели на соответствующую колонку результата. Так платформа определяет, по какому измерению публиковать фильтр.

 

Входящая фильтрация запускает пересчёт виджета только в автоматическом режиме обновления. В ручном режиме смена фильтров учитывается при следующем нажатии кнопки обновления.

Авторизация и безопасность данных

Доступ к данным модели

Для обращения к данным модели через client.query указывать токен не требуется: платформа выполняет запрос от имени текущего пользователя. Токен остаётся на стороне платформы и в скрипт не передаётся.

К данным применяются политики доступа модели:

  • RLS (Row-Level Security) — скрипт получает только те строки, к которым у пользователя есть доступ.

  • OLS (Object-Level Security) — таблицы, столбцы и меры, закрытые для пользователя политикой OLS, скрипту недоступны.

Доступ к внешним сервисам

Авторизация во внешнем сервисе реализуется средствами скрипта — например, заголовком Authorization или ключом API в параметрах запроса.

Параметры доступа (логины, ключи, токены) указываются непосредственно в тексте скрипта. Защищённое хранилище секретов (vault) в настоящий момент не поддерживается. Токен платформы для авторизации во внешних сервисах недоступен.

Среда выполнения и ограничения

Скрипты выполняются на Python 3.11 в изолированной среде. На выполнение скрипта действуют ресурсные ограничения:

Ресурс

Значение по умолчанию

Ресурс

Значение по умолчанию

Оперативная память

1800 МБ

Процессорное время

60 секунд

Общее время выполнения (включая ожидание ответа внешних сервисов)

180 секунд

Размер файла

100 МБ

Значения лимитов настраиваются администратором платформы.

Ограничения по объёму данных

Единого лимита на число строк нет — он зависит от этапа:

  1. Данные из модели (client.query) — ограничены так же, как выгрузка в Excel (той же константой в конфигурации платформы). При превышении запрос к Formula Engine не выполнится. Поэтому агрегируйте в запросе через measures, а не получайте сырые строки.

  2. Данные по REST — ограничения на число строк нет. Но всё полученное хранится в оперативной памяти скрипта (см. лимит памяти выше).

  3. Результат скрипта (return) → виджет — ограничен возможностями отрисовки виджета: на виджет передаётся не более ~10 000 строк, а многие визуализации «захлёбываются» заметно раньше (например, круговая диаграмма начинает подвисать уже на нескольких сотнях точек). В return отдавайте агрегированный, готовый к отображению результат.

Память. Все данные, которые скрипт загрузил или создал (результаты client.query, ответы REST, промежуточные DataFrame), одновременно хранятся в оперативной памяти — в пределах 1.8 ГБ на один скрипт.

Как распределять нагрузку

  • агрегацию больших объёмов выполняет Formula Engine — через client.query с агрегатами в measures (или средствами DAX-виджета);

  • Pandas применяйте для финальных преобразований и вычислений на уже агрегированных или ограниченных данных: ранжирование, скользящее среднее, расчёт долей, объединение с внешними данными и т.п.

Не загружайте в Python большие объёмы сырых данных из модели ради последующей агрегации в Pandas — агрегацию выполняет Formula Engine. Pandas работает с уже подготовленными данными.

Ошибки в скрипте

Если скрипт завершается с ошибкой, в редакторе (а на дашборде — в области виджета вместо визуализации) отображается traceback с пользовательскими кадрами — строками вашего скрипта. Служебные кадры платформы из него исключены, чтобы ошибку было удобнее искать.

Типовые ситуации отдаются отдельными понятными сообщениями: превышение лимитов памяти или времени, сбой инициализации среды выполнения, а также случай, когда скрипт не вернул результат.

Примеры

Динамика котировок выбранной компании (контекст + REST)

import pandas as pd import requests # 1) Фиксируем Тикер, который выбрал пользователь: selection_df = client.query(rows=["'спр Компании'[Тикер]"]).to_pandas() ticker = selection_df["Тикер"].tolist()[0] if len(selection_df) == 1 else "SBER" # 2) Фиксируем период из фильтра по датам (ISO → YYYY-MM-DD); иначе дефолт filters = context.filters.all() period = next((f for f in filters if getattr(f, "start", None) or getattr(f, "end", None)), None) date_from = (getattr(period, "start", None) or "2024-01-01")[:10] date_to = (getattr(period, "end", None) or "2026-12-31")[:10] # 3) REST к MOEX ISS url = f"https://iss.moex.com/iss/history/engines/stock/markets/shares/boards/TQBR/securities/{ticker}.json" resp = requests.get(url, params={"from": date_from, "till": date_to, "iss.meta": "off", "history.columns": "TRADEDATE,CLOSE"}, timeout=15) data = resp.json()["history"]["data"] return pd.DataFrame(data, columns=["Дата", "Цена"])

Структура портфеля по секторам (агрегация + REST)

import pandas as pd import requests from datetime import date, timedelta # Виджет «Структура портфеля» (кольцевая диаграмма по секторам). # Стоимость вложения в компанию = остаток её акций × цена акции на сегодня; # затем стоимость суммируется по секторам — получаем доли секторов в портфеле. # 1. Остаток акций по каждой компании = сколько купили минус сколько продали positions = client.query( rows=["'спр Компании'[Тикер]", "'спр Компании'[Сектор]"], measures={"'Акции'[Покупка]": "Sum", "'Акции'[Продажа]": "Sum"}, ).to_pandas() positions["Остаток, шт"] = positions["Покупка"].fillna(0) - positions["Продажа"].fillna(0) positions = positions[positions["Остаток, шт"] > 0] # только бумаги, что сейчас в портфеле # 2. Цена каждой акции НА СЕГОДНЯ (цена закрытия за последний торговый день) today = date.today() window_from = (today - timedelta(days=10)).isoformat() # запас на выходные/праздники def price_today(ticker): url = f"https://iss.moex.com/iss/history/engines/stock/markets/shares/boards/TQBR/securities/{ticker}.json" rows = requests.get(url, params={"from": window_from, "till": today.isoformat(), "iss.meta": "off", "history.columns": "TRADEDATE,CLOSE"}, timeout=15).json()["history"]["data"] closes = [close for _, close in rows if close is not None] return closes[-1] if closes else None positions["Цена, ₽"] = positions["Тикер"].map(price_today) # 3. Стоимость пакета каждой компании = остаток × цена на сегодня positions["Стоимость, ₽"] = positions["Остаток, шт"] * positions["Цена, ₽"] # 4. Структура портфеля по секторам (данные для кольцевой диаграммы) structure = positions.groupby("Сектор", as_index=False)["Стоимость, ₽"].sum() structure["Стоимость, ₽"] = structure["Стоимость, ₽"].round(2) return structure

Часто задаваемые вопросы

Сохраняются ли полученные данные в хранилище (DWH)?
Нет. Результат скрипта используется только для отрисовки виджета: он не записывается в хранилище данных и не попадает в модель. При каждом обновлении виджета скрипт выполняется заново.

Можно ли использовать полученные данные в других виджетах?
Нет. Скрипт привязан к своему виджету, и его результат недоступен другим виджетам. Между виджетами передаётся только фильтр (кросс-фильтрация), но не сами данные.

Установка сторонних библиотек (для администратора платформы)

Дополнительные пакеты подкладываются на хост в отдельную папку — без пересборки образа и без обращения к PyPI в момент выполнения (это работает и в закрытых контурах). Папка монтируется в контейнер только для чтения и прописана в PYTHONPATH, поэтому скрипты видят пакеты обычным import.

  1. Один раз соберите пакеты со всеми зависимостями под тем же базовым образом, что и python-service:

    mkdir build docker run --rm -v "$PWD/build:/build" <базовый-образ-python-service> \ pip install --target=/build prophet statsmodels
  2. Перенесите содержимое в папку customlibs хоста (${PERSISTENT_STORAGE_FOLDER}/python-service/customlibs):

    sudo cp -ra build/* <PERSISTENT_STORAGE_FOLDER>/python-service/customlibs/ sudo chmod -R a+rX <PERSISTENT_STORAGE_FOLDER>/python-service/customlibs

В контейнере папка доступна как /opt/customlibs. Перезапуск не требуется — новые пакеты увидит следующий запуск виджета.


Смотрите также

Работа с виджетами
Создание дашбордов и листов