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

Разработка в блоке Python

Особенности разработки

Важно понимать как работает указание областей видимости locals() и globals(). Пользователю в коде доступно для использования только locals(), т.е. переменные и функции, только того контекста, который он сформировал сам. В коде не указывается ограничений на подключаемые библиотеки - единственное - они должны быть установлены на серверной части в момент запуска приложения.

Пользователь может подключить через import любую библиотеку из списка подключенных, а так же системные библиотеки. Потенциально - это может вызывать проблемы безопасности, так что блок Python НЕЛЬЗЯ делать доступным, если система находится в общем доступе.

Реализация механизма без песочницы, код будет выполняться в потоке выполнения графа, вместе с основным.

Принципы выполнения кода и конструкция sleep()

Важная информация

Важным моментом является то, что при выполнении кода Python блок забирает на себя процессорное время и если код выполняется вычислительно "тяжелый" (heavy CPU bound operation) - это можно привести к блокировке того процесса, в котором идет выполнение. По умолчанию, ядро системы выполняется на нескольких workers - эта настройка задается в Gunicorn сервере, при старте приложения. В случае тяжелых вычислений, запущенных несколько раз, может возникнуть ситуация, когда на обработку сообщений пользователей не останется процессорного времени, что приведет к таймаутам ответа сервера и неработоспособности приложения.

Для избегания таких проблем в коде Python блока можно использовать конструкцию sleep(x) (from gevent import sleep), которая позволит остановить выполнение кода и передать управление для обработки запросов пользователей к приложению. Использовать ее рекомендуется, когда есть циклы или есть возможность приостанавливать выполнение. В любом из этих случаев ответственность за код лежит на прикладном разработчике.

Принципы обработки ошибок в f-string выражениях

Важная информация

В связи с особенностями обработки f-string в питоне, необходимо учитывать, что при возникновении ошибки внутри такой строки, например синтаксической, питон выдаст позицию внутри этой строки. Из-за особенностей работы внутри exec, который выполняет пользовательский код нельзя определить в какой библиотеке произошла ошибка и понять что это fstring. Поэтому в случае ошибки будет видна только строка, но она может не совпадать с той, что в коде.

Обработка исключений

Подробности по обработке системных исключений описаны здесь

По возможности не используйте fstring конструкции внутри блока питон.

Пример ошибочного кода
in1 = execution_context.inputs[0].value
null = None

out = f"""function randomIntFromInterval(min, max) { // min and max included   # ошибка будет в этой строке, в отладке будет указано, что это строка 1, хотя в файле это строка 4
    return Math.floor(Math.random() * (max - min + 1) + min)
}

const rndInt = randomIntFromInterval(100, 200)
var option = {
    xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    },
    yAxis: {
        type: 'value'
    },
    series: [
        {
            data: [150, 230, 224, 218, 135, 147, rndInt],
            type: 'line'
        }
    ]
};

var dom = this.Element.nativeElement;
dom.style.width = '100%';
dom.style.height = '100%';
var myChart = window['echarts'].init(dom);
myChart.on('rendered', (params) => {
    this.IsLoaded = true;
});
var app = {};

if (option && typeof option === 'object') {
    myChart.setOption(option);
}

this.resize = myChart.resize;

window.addEventListener('resize', myChart.resize);

myChart.on('click', (params) => {
    this.OnGoToVisualizerEventHandler('fd347e74-6cb4-4e16-b29a-eb2128026308');
    console.log(params);
    if (params.componentType === 'series') {
        const opts = myChart.getOption();
        const series = opts.series;
        const datasets = opts.datasets;
        const formData = {
            series: [],
            index: []
        };
        const si = params.seriesIndex;
        const di = params.dataIndex;
        const sr = series[si];
        const serie = { values: sr.data, options: sr.options };
        formData.series.push(serie);
        formData.index.push(di);
        var data = {
            id: this.Visualizer.ID,
            data: formData
        };
        this.OnPointClickEventHandler(data);
    }
});

// window.setTimeout(() => { myChart.resize; }, 300);'''
'''else:
out = {
"xAxis": {
"type": "category",
"data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
},
"yAxis": {
    "type": "value"
},
"series": [{
"data": [null],
"type": "line"
}]
}"""

execution_context.output_connectors[0].value = out
Пример кода
input_1 = execution_context.inputs[0].value
input_2 = execution_context.get_input_by_id('in2').value
input_2_id = execution_context.get_input_by_id('in2').id

for elem in input_1:
    elem['attrs']['new_awsome_attr'] = 'id выхода 2 = {0}'.format(input_2_id)

execution_context.add_output('out1', input_1 + input_2)
Пример структуры входных данных для типа Ряд данных
{
  "_params": {
    "fkey": [
        "date"
    ],
    "lazy": [],
    "ts": [
      {
        "frequency": "d",
        "key": "date",
        "mask": "%d.%m.%Y"
      }
    ],
    "vl": "vl"
    },
    "_source": "aaa479fd-db4e-42b3-a901-86fac8789edc",
    "attrs": {
    "arima": null,
    "model_type": "TREND",
    "trend": null
    },
    "keys": [
        "01.01.2020",
        "01.02.2020",
        "01.03.2020"
    ],
    "pkey": {
    "date_level": 7,
    "name": "X2"
    },
    "vl": [
        100,
        141.435621101643,
        159.417019107874
    ]
}

Основные концепты работы блока

  • Возможны импорты библиотек, которые подключены к расчетчику на этапе сборки или являются системными. Также возможны импорты пользовательских скриптов, хранящихся в разделе Скрипты;
  • Блок может работать с любыми типами входов или выходов;
  • Работа со всеми типами входов происходит в их нативном формате через execution_context.inputs[index].value;
  • Контроль корректности форматов не производится;
  • При работе со входами, если передается массив - он будет доступен в коде как массив;
  • При работе необходимо учитывать, что если один из входов не передан (пустой, или не рассчитан), то доступа к нему внутри кода не будет - расчетчик не передает его как вход;

Доступ ко входам и execution_context

Основным элементом для доступа к работе с входами/выходами и функциями ядра является переменная execution_context

Для доступа к входам и выходам используются следующие конструкции:

Входы

input_value = execution_context.inputs[0].value
input_value = execution_context.get_input_by_id('id').value

Выходы

execution_context.add_output('out1', 'some output value')

Выходы в коде всегда создаются динамически

Для использования этой переменной можно вызывать автодополнение (CTRL+Пробел) с основными конструкциями.

Лог расчета и прогресс

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

Лог расчета

В лог выводятся сообщения пользователя в виде обычной строки, форматирование доступно обычными средствами питона.

Команды для работы с логом
execution_context.log.info('Пример выдачи в лог записи с важностью info')
execution_context.log.warning('Пример выдачи в лог записи с важностью warning - будет помечено желтым')

# записи с ошибкой могут быть для информации и могут останавливать расчет принудительно
# для этого надо передать флаг stop
# при этом расчет будет остановлен на этой строке, в лог пойдет ошибка, весь расчет будет помечен как ошибочный
execution_context.log.error('Пример error и блок не ошибочный, выполнение продолжается')
execution_context.log.error('Пример error и блок не ошибочный, выполнение продолжается', stop=True)

Прогресс расчета

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

Проценты задаются с 0 до 100. При указании других значений будет взята ближайшая граница.

Команды для работы с прогрессом расчета блока
# инициализация прогресса, необязательна, но можно начать выполнение не с 0 процентов
execution_context.progress.initialize(0)
# увеличение на N процентов
execution_context.progress.increase(10)
# установка конкретного процента
execution_context.progress.value = 30
# увеличение на 1%, до 100%
execution_context.progress.increase()

Так же, через прогресс можно отслеживать прервал ли пользователь выполнение блока.

Приложение не может самостоятельно прервать расчет блока Python. Чтобы прервать расчет, необходимо в коде указывать проверки на прерывание пользователем. Для этого необходимо проверить что в коде есть прерывание. После этого можно корректно завершить выполнение своего кода, например, обработать транзакцию.

Пример прерывания кода
calc_stopped = execution_context.progress.is_stopped()
if calc_stopped:
# выполняем вызов остановки и завершаем выполнение своего кода корректно
# после того, как пользователь нажал на остановку - лога он не увидит, так что выводить сообщения тут бессмысленно
# тут надо выполнить корректное завершение расчета, информация в лог попадает при вызове остановки
# остановка вызовет ошибку расчета ErrorCodes.StoppedByUser и положит ее в лог пользователя и системы
# эта ошибка так же вызовет остановку всего расчета, в т.ч. расчета графа - но блок свой расчет завершит
execution_context.progress.do_stop()

Информация о контексте выполнения

В процессе выполнения блока возможно получение дополнительной информации о расчете и пользователе, который его запустил:

Получение информации о пользователе (словарь)
user = execution_context.user_info
>> user: {
"id": "105a9b07-fb45-4506-b519-fae19088df9b",
"login": "kplotnikov",
"fname": "Konstantin",
"lname": "Plotnikov",
"patronym": "",
"email": "",
"groups": ["Разработчики"]
}
Получение информации о задаче (словарь)
task = execution_context.task_info
>> task: {
"id": "d533506a-8e81-4f67-8369-bf574096085b",
"name": "VISMIND-6146 Блок Python. Текущий пользователь",
"desc": "Новая задача от 05.10.2021 11:44:30",
"created": "05.10.2021 11:44:30",
"updated": "05.10.2021 11:47:11",
"author": "Konstantin Plotnikov"
}
Получение идентификатора расчета (строка, uuid)
calc_id = execution_context.calculation_id
>> calc_id: "26fb78c5-39b9-4bdf-b928-07a788a0cd8f"
Получение информации о версии блока (целое число)
version = execution_context.block_version
>> version: 1

Работа с файловой системой

Этот функционал позволяет работать с файловой системой приложения. Использование этого функционала потенциально небезопасно.

При необходимости генерации файла можно воспользоваться функционалом, который предоставляет библиотека docx.

Возможно формировать таким образом только docx файлы

execution_context.save_bytes

Для формирования файла используйте следующий пример (использует внутреннюю библиотеку vmResource)

execution_context.save_bytes(
    data: BinaryIO,
    name: str,
    ext: str,
    add_timestamp: bool = True,
    timezone: Optional[int] = None
) -> str

Метод позволяет сохранить файл в бинарном виде на файловый сервер. В ответе придет путь до объекта (внутри задачи).

Все файлы загружаются в папку /doc

Параметры:

  • data - данные файла.
  • name - наименование файла, без расширения.
  • Если добавить в наименование файла разделитель /, то можно указать папку, в которой нужно будет сохранить файл (корневой каталог /doc при этом останется), к примеру:
    • file - будет сохранен по пути /doc/file.ext
    • folder/file- будет сохранен по пути /doc/folder/file.ext
  • ext - расширение файла.
  • add_timestamp - флаг добавления временной отметки к имени файла.
  • Временная отметка будет отображена в формате --%Y-%m-%d--%H-%M-%S, к примеру someFileName--2024-02-01--13-29-29.docx.
  • timezone - позволяет настроить часовой пояс для временной отметки файла (если она включена). Целое число от -12 до 14. Если не указано для временной отметки будет использовано время сервера.
Пример вызова функции
import io
import json
from docx import Document

# Cоздаем и заполняем docx документ
document = Document()
document.add_heading('Document Title', 0)
p = document.add_paragraph('A plain paragraph having some ')
p.add_run('bold').bold = True
p.add_run(' and some ')
p.add_run('italic.').italic = True

# Создаем экземпляр BytesIO
file_stream = io.BytesIO()
# Сохраняем документ в байты
document.save(file_stream)

# Сохраняем документ в хранилище
f = execution_context.save_bytes(
    data=file_stream,
    name='test_file_6', 
    ext='docx',
    timezone=5
)

execution_context.get_bytes

execution_context.get_bytes(
    path: str
) -> BinaryIO

Метод позволяет получить файл в бинарном виде по его наименованию.

Параметры:

  • path - путь до файла с наименованием и расширением.

Для опытных пользователей

Доступны еще два не обязательных параметры:

  • additional_path - дополнительный путь, который необходимо указать. По умолчанию путь не указывается.
  • task_id - идентификатор задачи, в которой находится файл. По умолчанию используется задача, в которой расположен блок.

Работа с Pandas

Библиотеки pandas корректно распознают бинарные файлы, переданные для чтения, к примеру в метод read_csv.

Пример вызова функции

import pandas as pd

path = execution_context.inputs[0].value['fileNames'][0]

# Получаем бинарный файл
binary_file = execution_context.get_bytes(path=path)
# Читаем полученный файл через Pandas
xl = pd.read_csv(binary_file)

# Получаем результат и выдаем на выход блока
out=xl.values.tolist()
execution_context.output_connectors[0].value = out

Выход из кода расчета

Для того чтобы из кода можно было выйти, надо вызвать метод _return у execution_context. Расчет блока остановится и продолжит считать следующие за блоком блока. Если же передать в _return сообщение об ошибке, то расчет на блоке остановится и блоки за ним считаться не будут.

Параметры: - message: str - текст сообщения - message_type: str - тип сообщения. Могут быть: 'info', 'warning', 'error'. По умолчанию 'info'

Так же можно передать в message_type тип сообщения через execution_context: - execution_context.message_type.INFO - execution_context.message_type.WARNING - execution_context.message_type.ERROR

Примеры вызовов:

Выход из расчета, расчет блоков за блоком продолжится
execution_context.exit()
Выход из расчета с сообщением в лог, расчет блоков за блоком продолжится
execution_context.exit(message="Это сообщение отобразится в логе блока")
Выход из расчета с сообщением предупреждения в лог, расчет блоков за блоком продолжится
execution_context.exit(message="Это сообщение отобразится в логе блока", message_type='warning')
Выход из расчета с сообщением об ошибке в лог, расчет блоков за блоком производиться не будет
execution_context.exit(message="Это сообщение отобразится в логе блока", message_type='error')

Таймзона

Для того чтобы из кода получить информацию по таймзоне, надо использовать метод timezone у execution_context.

Методы у timezone: - name - наименование таймзоны. Пример: "Asia/Yekaterinburg" - offset - offset таймзоны в формате "[+-][часы][минуты]". Пример: "+0500" - значит +5 часов - seconds - offset таймзоны в секундах. Пример: 18000 - info - поле для информации откуда берется таймзона - user - таймзона передана с фронта - server - таймзона взята с бэка (таймзона сервера)

Примеры вызовов:

Получение информации по таймзоне
execution_context.user_timezone.name
execution_context.user_timezone.offset
execution_context.user_timezone.seconds
execution_context.user_timezone.info

Работа с библиотекой vmResource

Для обращения к функциям ядра используется библиотека vmResource. Подробнее про ее использование - Модуль vmResource.

Работа с ядром платформы

Внутри блока Python можно обращаться к другим функциям ядра плафтормы. Подробнее Основные классы.

Работа с библиотекой локальной разработки

См. раздел Пакет vm-local-developer.

Список библиотек, доступных в блоке

Список библиотек можно посмотреть на странице Внешние зависимости и библиотеки.

Импорты пользовательских скриптов

Блок поддерживает импорты модулей, написанных пользователями. Это позволяет переиспользовать код в нескольких блоках и задачах. Чтобы подключить пользовательский скрипт в блоке Python, нужно выполнить следующий импорт:

Импорт пользовательского модуля

from vmscripts.module1 import func1

Здесь vmscripts - имя библиотеки отвечающей за динамический импорт модуля пользователя, а module1 – название пользовательского скрипта.

Создание и редактирование скриптов

Создание и редактирование выполняется на странице администрирования "Скрипты". Подробнее о нём в разделе Скрипты

Ограничения импортов

  1. Команда import vmscripts не поддерживается, потому что она подразумевает загрузку всех пользовательских скриптов в момент импорта;
  2. Скрипты должны иметь уникальное имя модуля;
  3. В пользовательских скриптах нельзя выполнить импорты других скриптов;
  4. По умолчанию модули привязаны к задачам и доступны для импорта в рамках одной задачи. Но модуль можно сделать глобально доступным для всех задач, задав соответствующий флаг в разделе "Скрипты" на странице администрирования.

Удаленный запуск

Блок можно рассчитывать в удаленной, изолированной "песочнице" внутри docker контейнера. Подробнее см. Песочница и удаленный запуск расчетов

Пример блока
import time

val1 = execution_context.get_input_by_id('in1').value
execution_context.progress.initialize(0)

# info не помечает блок как ошибочный
execution_context.log.info('Пример info - Расчет блока запущен')
time.sleep(5)
execution_context.progress.increase(10)
execution_context.log.info('Пример info - Шаг 0, прогресс = 10')
time.sleep(5)
execution_context.progress.value = 30
execution_context.log.info('Пример info - Шаг 1, прогресс = 30')
time.sleep(5)
execution_context.progress.increase()
execution_context.log.info('Пример info - Шаг 2, прогресс + 1')

# warning не помечает блок как ошибочный
execution_context.log.warning('Пример warning - На входе {0} рядов'.format(len(val1)))
time.sleep(5)
# таймаут на 20 секунд, пользователь может вызывать остановку расчета
execution_context.log.info('Пауза на 20 секунд - Можете вызывать остановку расчета')
time.sleep(20)
execution_context.log.info('Проверяем был ли расчет блока остановлен')
calc_stopped = execution_context.progress.is_stopped()
if calc_stopped:
    # выполняем вызов ошибки и завершаем выполнение своего кода корректно
    # после того, как пользователь нажал на остановку - лога он не увидит,
    # так что выводить сообщения тут бессмысленно
    # тут надо выполнить корректное завершение расчета, информация в лог попадает при вызове остановки
    # остановка вызовет ошибку расчета ErrorCodes.StoppedByUser и положит ее в лог
    execution_context.progress.do_stop()

execution_context.log.info('Остановки не было')
# выводим результат
res = val1
execution_context.add_output('out1', res)
execution_context.progress.value = 90

Работа с глобальными переменными

Важная информация

Будьте осторожны с переопределением глобальных переменных библиотек, (таких как например nympy.inf) т.к. это повлияет на все приложение. Например, с помощью библиотеки pandas можно изменить переменную nympy.inf в nan

Пример, как делать нельзя
import pandas as pd
import numpy as np

pd.set_option('use_inf_as_na', True)

df = pd.DataFrame([[1, 2], [2, np.nan], [3, np.inf]], columns=['col1', 'col2'])
df = df.dropna(subset=['col1', 'col2'], how='all')

Вместо него нужно сделать так:

Пример, который можно использовать
import pandas as pd
import numpy as np

df = pd.DataFrame([[1, 2], [2, np.nan]], columns=['col1', 'col2'])
with pd.option_context('mode.use_inf_as_na', True):
    df = df.dropna(subset=['col1', 'col2'], how='all')

Работа с конструкциями управления

Важная информация

Будьте осторожны с использованием конструкций управления выполнением, т.к. расчет блока происходит в контексте выполнения приложения (в т.ч. расчет событий).
Например, с помощью функции exit() будет завершено выполнение процесса целиком, после чего воркер, на котором происходило выполнение отключится.