Разработка в блоке 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
Выходы
Выходы в коде всегда создаются динамически
Для использования этой переменной можно вызывать автодополнение (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()
Информация о контексте выполнения
В процессе выполнения блока возможно получение дополнительной информации о расчете и пользователе, который его запустил:
Получение информации о пользователе (словарь)
Получение информации о задаче (словарь)
Получение идентификатора расчета (строка, uuid)
Получение информации о версии блока (целое число)
Работа с файловой системой
Этот функционал позволяет работать с файловой системой приложения. Использование этого функционала потенциально небезопасно.
При необходимости генерации файла можно воспользоваться функционалом, который предоставляет библиотека 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.extfolder/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
Метод позволяет получить файл в бинарном виде по его наименованию.
Параметры:
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
Примеры вызовов:
Выход из расчета с сообщением в лог, расчет блоков за блоком продолжится
Выход из расчета с сообщением предупреждения в лог, расчет блоков за блоком продолжится
Выход из расчета с сообщением об ошибке в лог, расчет блоков за блоком производиться не будет
Таймзона
Для того чтобы из кода получить информацию по таймзоне, надо использовать метод timezone у execution_context.
Методы у timezone: - name - наименование таймзоны. Пример: "Asia/Yekaterinburg" - offset - offset таймзоны в формате "[+-][часы][минуты]". Пример: "+0500" - значит +5 часов - seconds - offset таймзоны в секундах. Пример: 18000 - info - поле для информации откуда берется таймзона - user - таймзона передана с фронта - server - таймзона взята с бэка (таймзона сервера)
Примеры вызовов:
Получение информации по таймзоне
Работа с библиотекой vmResource
Для обращения к функциям ядра используется библиотека vmResource. Подробнее про ее использование - Модуль vmResource.
Работа с ядром платформы
Внутри блока Python можно обращаться к другим функциям ядра плафтормы. Подробнее Основные классы.
Работа с библиотекой локальной разработки
См. раздел Пакет vm-local-developer.
Список библиотек, доступных в блоке
Список библиотек можно посмотреть на странице Внешние зависимости и библиотеки.
Импорты пользовательских скриптов
Блок поддерживает импорты модулей, написанных пользователями. Это позволяет переиспользовать код в нескольких блоках и задачах. Чтобы подключить пользовательский скрипт в блоке Python, нужно выполнить следующий импорт:
Здесь vmscripts - имя библиотеки отвечающей за динамический импорт модуля пользователя, а module1 – название
пользовательского скрипта.
Создание и редактирование скриптов
Создание и редактирование выполняется на странице администрирования "Скрипты". Подробнее о нём в разделе Скрипты
Ограничения импортов
- Команда
import vmscriptsне поддерживается, потому что она подразумевает загрузку всех пользовательских скриптов в момент импорта; - Скрипты должны иметь уникальное имя модуля;
- В пользовательских скриптах нельзя выполнить импорты других скриптов;
- По умолчанию модули привязаны к задачам и доступны для импорта в рамках одной задачи. Но модуль можно сделать глобально доступным для всех задач, задав соответствующий флаг в разделе "Скрипты" на странице администрирования.
Удаленный запуск
Блок можно рассчитывать в удаленной, изолированной "песочнице" внутри 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
Пример, как делать нельзя
Вместо него нужно сделать так:
Пример, который можно использовать
Работа с конструкциями управления
Важная информация
Будьте осторожны с использованием конструкций управления выполнением, т.к. расчет блока происходит в контексте
выполнения приложения (в т.ч. расчет событий).
Например, с помощью функции exit() будет завершено выполнение процесса целиком, после чего воркер, на котором
происходило выполнение отключится.