Личный веб-сервер на 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» можно увидеть вот такую картинку:

image

На этом всё и удачного дня!

© Habrahabr.ru