Личный веб-сервер на Wolfram Language
Иногда людям хочется быстро сделать веб-сервер, корневая логика которого будет на Wolfram Language. Существует правильный и долгий путь. Наградой будет красота решения и производительность. И существует второй путь. О нем мы и поговорим.
Я начал активно изучать Mathematica и Wolfram Language где-то полгода назад и сразу возникло желание использовать его как «повседневный» язык для разных бытовых и околорабочих задач. Знаете, у каждого есть язык, который первым приходит на ум, если нужно, скажем, проанализировать какую-то коллекцию данных или связать друг с другом несколько систем. Обычно это какой-то достаточно высокоуровневый скриптовый язык. В моем случае в этой роли выступал Python, но тут у него появился серьезный конкурент.
Однако не все можно решить, запустив блокнот Mathematica и разово выполнив код из него. Некоторые задачи требуют периодического исполнения либо запуска по какому-то событию. Нужен сервер. Для начала посмотрим, какие варианты развертывания и исполнения предлагает сама компания. Насколько я могу судить, опции следующие:
1) Старый добрый Mathematica Notebook. Иными словами, разовая рабочая сессия в GUI.
2) Wolfram Cloud. И это замечательная опция, которую использую в том числе и я. Однако есть масса причин, по которым вариант с облаком может не подойти. Назову лишь одну из них — каждый вызов стоит ненулевое количество денег. Для множества мелких периодических операций это может быть неоправданно затратно, особенно когда под рукой есть простаивающие мощности.
3) Wolfram Private Cloud. Звучит как какая-то грядущая возможность запустить собственное облако. Подробности мне неизвестны
4) Использовать Wolfram Symbolic Transfer Protocol. Выглядит как самый основательный и универсальный способ интеграции Wolfram Language в вашу систему. Сервер здесь — лишь один из частных случаев применения. Тот самый «правильный и долгий путь».
5) Wolfram Script. Все просто — вызываем код на Wolfram Language как любой другой скрипт, без непосредственного участия графического интерфейса. Cron, pipeline и все остальные замечательные механизмы в нашем распоряжении. Этот способ мы и используем для быстрого создания сервера.
Непосредственно сервером мы можем выбрать что угодно, в моем случае это Tornado. Напишем простейший handler, который будет отправлять аргументы, заголовки и тело запроса в наш скрипт и считывать результаты его работы.
import tornado.ioloop
import tornado.web
import os, subprocess
import json
WOLFRAM_EXECUTABLE = "wolfram"
def execute(arguments):
def run_program(arguments):
p = subprocess.Popen(arguments,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
return iter(p.stdout.readline, b'')
res = ''
for line in run_program(arguments):
res+=line
return res
class MainHandler(tornado.web.RequestHandler):
def get(self):
out = execute([WOLFRAM_EXECUTABLE,"-script", "main.m",
str(self.request.method),
str(json.dumps(self.request.arguments)),
str(json.dumps(self.request.headers)),
str(self.request.body)])
self.write(out)
application = tornado.web.Application([
(r"/", MainHandler),
])
application.listen(8888)
Собственно, «main.m» — это и есть наш скрипт на Wolfram Language. В нем нам нужно получить и интерпретировать переданные аргументы, а также вернуть результат.
method = $CommandLine[[4]]
arguments = Association @ ImportString[$CommandLine[[5]], "JSON"]
headers = Association @ ImportString[$CommandLine[[6]], "JSON"]
body = If[Length[$CommandLine] >= 7,$CommandLine[[7]], ""]
Print["Hello world"]
Наш скрипт выводит «Hello world». Часть на питоне, в свою очередь, честно возвращает эти данные клиенту.
В принципе, в этом вся суть метода.
В таком виде наш сервер сможет принимать и возвращать только строковые данные с кодом результата 200. Хочется немного больше гибкости. Для этого данные из скрипта должны передаваться не просто в виде строки, а в каком-то структурированном виде. Так у нас появляется еще одно преобразование в JSON и обратно. Формат будет таким:
{
"code”: 200,
"reason”: OK,
"body”: "Hello world"
}
Теперь его нужно корректно обработать на другой стороне.
outJson = json.loads(out)
self.set_status(outJson["code"], outJson["reason"])
if(outJson["body"] != None):
self.write(str(outJson["body"]))
Следующим шагом будет добавление возможности возвращать не только текст, но и другие данные. Возможно, два двойных преобразования JSON казались кому-то недостаточно медленным решением… Добавим в наш JSON поля «file» и «contentType». Если поле «file» непустое, то вместо записи в поток вывода содержимого поля «body» мы считываем указанный файл.
outJson = json.loads(out)
self.set_status(outJson["code"], outJson["reason"])
if(outJson["file"] != None):
self.add_header("Content-Type", outJson["contentType"])
with open(outJson["file"], 'rb') as f:
while True:
data = f.read(16384)
if not data: break
self.write(data)
self.finish()
os.remove(outJson["file"])
elif(outJson["body"] != None):
self.write(str(outJson["body"]))
Взглянем на это все со стороны вызываемого скрипта. Пара методов для генерации ответа:
AsJson[input_] := ExportString[Normal @ input, "JSON"]
HTTPOut[code_, body_, reason_] :=
<|"code"->code, "body"->body, "reason"->reason, "file"->Null|>
HTTPOutFile[expression_, exportType_, contentType_] :=
Module[{filePath = FileNameJoin[{$TemporaryDirectory, "httpOutFile"}]},
Export[filePath, expression, exportType];
<|"code"->200,
"body"->Null,
"reason"->Null,
"file"->filePath,
"contentType"->contentType|>
]
Наконец, напишем обработчики конкретных методов.
HTTPGet[arguments_, headers_] := AsJson[...]
Switch[method,
"GET", HTTPGet[arguments, headers],
"POST", HTTPPost[arguments, headers, body]]
Таким образом, появляются методы HTTPGet, HTTPost и аналогичные. Настало время для создания бизнес-логики. Можно создать обработчики для различных путей (»/»,»/SomeEndpoint» и т.д.), но вместо этого мы добавим к вызову аргумент, который будет определять вызываемую функцию:»/? op=MyFunction».
Осталось только добавить логику выбора и вызова этой функции в нашем скрипте. Используем ToExpression[].
HTTPGet[arguments_, headers_] :=
Module[{methodName = "GET"<>arguments["op"]},
AsJson[ToExpression[methodName][arguments, headers]]
]
Теперь можно просто добавить функцию GETMyFuction и первая единица бизнес-логики готова. Пусть эта функция выводит текущее время:
GETMyFuction[arguments_, headers_] :=
HTTPOut[ToString[Now]]
Осталось привести пример вывода картинки. И раз уж мы не используем входные параметры, обозначим их безымянным паттерном, который соответствует любому количеству элементов.
GETTestGraph[___] :=
Module[{},
out = Graph[{a -> e, a -> c, b -> c, a -> d, b->d, c->a}];
HTTPOutFile[out, "PNG", "image/png"]
]
Теперь, при открытии в браузере »…/? op=TestGraph» можно увидеть вот такую картинку:
На этом всё и удачного дня!