Houdini 19.0 Pythonスクリプト hou hou.webServer

hou.webServer.apiFunction HOM function

HoudiniのWebサーバー上のAPIエンドポイントを介してコール可能な関数のデコレータで、JSONレスポンスまたはバイナリレスポンスを返します。

On this page

apiFunction(namespace=None, return_binary=False, arg_types=None, ports=[])

このデコレータを使って関数をデコレート(修飾)すると、その関数は/apiエンドポイントを介してコール可能な関数として登録されます。

Note

デコレータは、関数名をAPI関数名として使用します。

namespace

この文字列を指定すると、その文字列が関数名の接頭辞として作用します。 例えば、関数名がvector_lengthnamespace="geo"であれば、ネームスペースが付いた関数名は"geo.vector_length"となります。 API関数をネームスペースで整理することを心がけてください。

return_binary

これがTrueの場合、関数の戻り値は常にバイナリデータとして扱われ、MIMEタイプがapplication/octet-streamで提供されます。

これがFalseの場合、bytes(Python3のみ)またはbytearrayのオブジェクトを返すことで、バイナリデータを返すこともできます。

arg_types

引数名をPythonの型にマッピングした辞書。例えば、{"x": int, "y": int, "z": int}

ports

オプションで、API関数にバインドさせたいポート番号を指定することができます。 ポート番号を指定しなかった場合は、サーバーのメインポートが使用されます。 そのメインポートにバインドさせたいのであれば、必ず'0'を使用してください。 実際のポート番号にバインドさせると、レジストリからそのAPI関数が削除されます。

サーバーは、クライアントからの引数に対して与えられた型をコールした後に、それらの引数を使って関数をコールします。 これは、クライアントがそれらの引数を文字列として指定する時に役立ちます(以下のコールする方法セクション下のAPIをコールする1番目の方法を参照してください)。

import hou


@hou.webServer.apiFunction(namespace="geo")
def vector_length(request, x, y, z):
    return hou.Vector3(x, y, z).length()

Note

サーバーがどのスレッドからAPI関数をコールするのかは保証されていないので、hou.frame()などのスレッド固有の結果に依存することはできません。

コールする方法

クライアントからこのAPIをコールする方法が2通りあります:

  • 1つ目の方法は、次のように/apiに対してPOSTメソッドを実行することです。このメソッドのcmd_functionパラメータには関数名を格納し、それ以外のすべてのパラメータには引数を格納します。例えば、cmd_function=vector_length&x=1&y=2&z=3です。

    requestsライブラリを使用したPythonクライアントコード

    import requests
    
    resp = requests.post("http://127.0.0.1:8008/api", {
        "cmd_function": "geo.vector_length",
        "x": "1", "y": "2", "z": "3"    
    })
    print(resp.text)
    

    この手法なら、クライアントの実装は簡単ですが、ハンドラーがすべての引数値を文字列として受信するので、ハンドラーにて型のチェック/変換をするコードをさらに追加するか、または、関数デコレータでarg_types辞書を指定する必要があります(上記参照)。

    Note

    型変換を追加しなかった場合、このサンプルは動作しないです。 型変換を用意したサンプルは、以下のデフォルト値と引数型を参照してください。

  • 推奨する2つ目の方法は、次のように/apiに対してPOSTメソッドを実行することです。このメソッドのjsonパラメータには、関数名の文字列、位置指定引数のリスト、キーワード引数が定義されたオブジェクトを含んだJSONオブジェクトの文字列エンコードを格納します。例えば、["geo.vector_length", (1, 2, 3), {}]です。

    この手法なら、型情報が暗黙的にエンコードされ、もっと複雑な引数型を格納することができます。

    追加のmultipart/form-data引数は、キーワード引数を追加で格納さるものと解釈されます。 この引数を使用することで、base64エンコードする必要なくバイナリデータを渡すことができます(jsonという名前のバイナリデータ引数を持つことはできません)。 webapiclient.pyを使用したPythonクライアントは、この詳細について気にする必要はありません。

    requestsライブラリを使用したPythonの最小限のクライアントコード

    import json
    import requests
    
    def call_api(endpoint_url, function_name, *args, **kwargs):
        return requests.post(endpoint_url,
            data={"json": json.dumps([function_name, args, kwargs])}).json()
    })
    

    Houdiniに同梱されているwebapiclient.pyモジュールは、API関数をコール可能な完全なPythonクライアントを実装しています。 このモジュールは、Houdiniに依存しておらず、あなた自身のプロジェクトにコピーすることができます。 このモジュールを使用すれば、もっと自然な構文を使用してAPI関数をコールすることができます。

    service = webapiclient.Service("http://127.0.0.1:8008/api")
    print(service.geo.vector_length(1, 2, 3))
    

    JavaScriptの最小限のクライアントコード

    function call_api(function_name, kwargs)
    {
        var formdata = new FormData();
        formdata.append("json", JSON.stringify(
            [function_name, [], kwargs]));
    
        return fetch("/api", {
            method: "POST",
            body: formdata
        });
    }
    

    バイナリデータを扱う完全なJavaScriptクライアントが以下に用意されています。

デフォルト値と引数型

  • API関数にはデフォルト値を持たせることができ、コール側は、JSONエンコードの[name, args, kwargs]コールの手法を使用した場合には、位置指定引数とキーワード引数を混在させて使用することができます。

    デフォルト値を使用したサーバーとPythonクライアントのサンプル。

    # サーバーコード:
    @hou.webServer.apiFunction("test")
    def foo(a, b=2, c=3):
        return (a, b, c)
    
    
    # クライアントコード:
    service = webapiclient.Service("http://127.0.0.1:8008/api")
    
    # 以下のコールはどれも同じで[1, 2, 3]を返します:
    service.test(1, 2, 3)
    service.test(1, 2, c=3)
    service.test(a=1, c=3, b=2)
    
  • URLエンコードでコールする手法を使用した場合、すべての引数が文字列として渡されますが、API関数を宣言する時に型を指定することが可能です。

    Python2とPython3で引数型を指定する

    @hou.webServer.apiFunction("test", arg_types={"value": int})
    def double_int(value):
        return value * 2
    
    # "value=3"を含んだPOSTボディを使ってコールすると、その文字列の"3"は整数の3に変換されます。
    

    Python3では、型のヒントと一緒に引数型を指定することもできます。

    @hou.webServer.apiFunction("test")
    def double_int(value: int):
        return value * 2
    

バイナリデータ

  • API関数は、bytearrayオブジェクト(Python3だとbytesオブジェクト)を返すことで、または、Content-Typeapplication/octet-streamのレスポンスを返すことで、 または、hou.webServer.fileResponse()を使用してapplication/octet-streamを利用するファイルの内容をストリーム化することで、 または、関数の戻り値がバイナリであることをマークしてbytesを返すことで、バイナリデータを返すことができます。

  • webapiclient.pyがバイナリデータの戻り値を受信すると、それをすべてメモリにロードする必要がないようにクライアント側でストリーム化する機会が得られるファイルライクなオブジェクトを返します。 クライアント側ですべてのデータを読み込まない場合に要求が閉じられるようにするために、クライアント側でその結果に対してwithステートメントを使用してください。

    バイナリデータを返すサーバーとそれを受信するPythonクライアント

    # サーバーは、以下のようにバイナリデータを返すことができます:
    
    @hou.webServer.apiFunction("test")
    def binary_data(request):
        return bytearray([0, 0, 65, 66, 0])
    
    
    # クライアントは、他の関数と同様に関数をコールすることができ、アンパックされたJSONの代わりに`bytes`オブジェクトを受信します:
    
    with service.test.binary_data() as result_stream:
        binary_data = result_stream.read()
    

    API関数の戻り値がバイナリであることをマークしたサーバー

    @hou.webServer.apiFunction("test", return_binary=True)
    def binary_data(request):
        return b'\x00\x00AB\x00'
    
  • webapiclient.pyを使用した場合、クライアントは、bytearrayを使用してバイナリデータを引数に渡すことができたり、webapiclient.Fileオブジェクトを使用してディスク上のファイルからバイナリデータをストリーム化することができます。 バイナリデータの引数は必ずキーワード引数を使用して渡す必要があり、位置指定引数で渡すことはできないことに注意してください。

    バイナリデータを送信するPythonクライアントとそれを受信するサーバー

    # クライアントは、以下のようにバイナリデータを送信することができます:
    service.test.use_binary_data(
        small_data=bytearray([0, 0, 65, 66, 0]),
        big_data=webapiclient.File("/path/to/binary/file"))
    
    
    # サーバーは、バイナリデータを[Hom:hou.webServer#UploadedFile]オブジェクトとして受信し、
    # そのオブジェクトからバイナリデータを読み込むことができます:
    import shutil
    
    @hou.webServer.apiFunction("test")
    def use_binary_data(request, small_data, big_data):
        small_data_as_bytes = small_data.read()
        with open("uploaded_file.bin", "wb") as open_file:
            shutil.copyfileobj(big_data, open_file)
    

エラー

  • 422ステータスを返すには、API関数からhou.webServer.APIErrorを引き起こします。

  • webapiclient.pyを使用した場合、200以外のステータスコードを持ったどのレスポンスも、クライアントに対してwebapiclient.APIError例外を引き起こします。

  • API関数がAPIError以外の例外を生成した場合、サーバーは500ステータスコードを返します。 サーバーがデバッグモードの場合、そのレスポンスにはスタックトレースが含まれます。

    エラーを説明したサーバーコードとPythonクライアントコード

    # サーバー:
    
    @hou.webServer.apiFunction("test")
    def illustrate_errors(request, value):
        if value == 0:
            return ["successful", "result"]
        if value == 1:
            raise hou.webServer.APIError("an error occurred")
        if value == 2:
            raise hou.webServer.APIError({"any": "json-encodable"})
        if value == 3:
            raise hou.OperationFailed("an unhandled exception")
    
    
    # クライアント:
    
    print(service.test.illustrate_errors(0)) # ["successful", "result"]がプリントされます。
    
    try:
        service.test.illustrate_errors(1)
    except webapiclient.APIError as e:
        print(str(e)) # "an error ocurred"がプリントされます。
        print(e.status_code) # 422がプリントされます。
    
    try:
        service.test.illustrate_errors(2)
    except webapiclient.APIError as e:
        print(e.args[0]["any"]) # "json-encodable"がプリントされます。
        print(e.status_code) # 422がプリントされます。
    
    try:
        service.test.illustrate_errors(3)
    except webapiclient.APIError as e:
        print(e.status_code) # 500がプリントされます。
    

サンプル

USD処理サービス

このサンプルでは、Houdiniを使用して、USDファイルを受信しサーバー上に保存されているSOP HDAを使ってそれらのUSDファイルを処理しその結果を返すWebサービスを構築する方法を説明しています。

このサンプルでは2つのAPI関数を用意しています。 1つ目のAPI関数では、クライアントとサーバーのどちらも入出力するUSDデータファイルが存在する同じファイルサーバーにアクセスすることができて、且つ、 クライアントがその入力ファイルの場所と目的の出力ファイルの場所を受け取ることを前提としています。 2つ目のAPI関数では、それらのファイルの実際の内容を受け渡し、クライアントのメモリを占有しないようにクライアントからの入力ファイルをストリーム化します。 サーバーは、自身のメモリを占有しないようにその入力ファイルをhou.webServer.UploadedFileとして受信し、Houdiniでその内容を読み込めるようにするためにまだディスク上にその内容が存在していなければそれをディスク上に保存し、 1つ目のAPI関数と同様にUSDファイルを処理し、その一時ファイル出力をストリームに戻します。 次に、クライアントはその受信した結果をディスクに流します。

usdprocessor.py

import os
import tempfile

import hou

import webutils


@hou.webServer.apiFunction("usdprocessor")
def process_file(request, input_file, hda_name, output_file):
    '''
    サーバーからアクセス可能な入力USDファイルのパスを与えると、
    そのファイルが読み込まれ、サーバー上に保存されている指定した名前のSOP HDAを使用してそのファイルを処理し、
    クライアントからアクセス可能なファイル場所にそのUSD出力を書き出します。
    '''
    # サーバーとクライアントが要求しているSOPアセットファイルを読み込みます。
    hda_file = os.path.join(
        webutils.script_file_dir(), "assets", hda_name + ".hda")
    try:
        sop_node_type_name = get_sop_node_type_from_hda(hda_file)
    except hou.OperationFailed as e:
        raise hou.webServer.APIError(e.instanceMessage())

    # 入力USDファイルを読み込むLOPネットワークを構築し、SOPを使用してそのファイルを処理し、
    # 新しいファイルに保存します。
    stage = hou.node("/stage")
    file_node = stage.createNode("sublayer")
    file_node.parm("filepath1").set(input_file)

    sopmodify = stage.createNode("sopmodify")
    sopmodify.parm("primpattern").set("*")
    sopmodify.parm("unpacktopolygons").set(True)
    sopmodify.setFirstInput(file_node)

    subnet = sopmodify.node("modify/modify")
    sop = subnet.createNode(sop_node_type_name)
    sop.setFirstInput(subnet.item("1"))
    subnet.displayNode().setFirstInput(sop)
    subnet.layoutChildren()

    rop = stage.createNode("usd_rop")
    rop.parm("lopoutput").set(output_file)
    rop.parm("savestyle").set("flattenstage")
    rop.setFirstInput(sopmodify)
    stage.layoutChildren((file_node, sopmodify, rop))

    # 結果を計算し、ノードにエラーがあればエラーを返します。
    rop.render()
    errors = "\n".join(sopmodify.errors() + rop.errors())
    if errors:
        raise hou.webServer.APIError(errors)


def get_sop_node_type_from_hda(hda_file):
    hou.hda.installFile(hda_file)
    definition = hou.hda.definitionsInFile(hda_file)[0]
    if definition.nodeTypeCategory() != hou.sopNodeTypeCategory():
        raise hou.OperationFailed("This asset is not a SOP")
    return definition.nodeTypeName()


@hou.webServer.apiFunction("usdprocessor", return_binary=True)
def process_data(request, usd_data, hda_name):
    '''
    サーバーにストリーム化されたUSDデータを処理し、USDデータをレスポンスとしてストリームに戻します。
    '''
    # `usd_data`は、サーバーにストリーム化されたUploadedFileのファイルライクなオブジェクトです。
    # そのデータが大きければディスク上に保存することができます。強制的にデータを一時的なディスク上のファイルに保存し、
    # その一時ファイルのデータを処理して、それをストリームに戻します。
    usd_data.saveToDisk()

    # その一時的な出力ファイルの名前を選択します。
    with tempfile.NamedTemporaryFile(
            suffix=".usd", delete=False) as open_output_file:
        temp_output_file = open_output_file.name

    try:
        process_file(
            request, usd_data.temporaryFilePath(), hda_name,
            temp_output_file)
    except:
        if os.path.exists(temp_output_file):
            os.unlink(temp_output_file)
        raise

    # データのストリーム化が終了した時にその一時的なファイルを削除するようにサーバーに要求します。
    return hou.webServer.fileResponse(
        temp_output_file, content_type="application/octet-stream",
        delete_file=True)


if __name__ == "__main__":
    hou.webServer.run(8008, debug=True)

webutils.py

import os
import inspect


def script_file_dir():
    '''
    サーバーの起動に使用されるスクリプトを含んだ辞書を返します。
    '''

    try:
        file_path = __file__
    except NameError:
        file_path = inspect.getfile(inspect.currentframe())

    if not os.path.isabs(file_path):
        file_path = os.path.normpath(os.path.join(os.getcwd(), file_path)))

    return os.path.dirname(file_path)

クライアントは、以下のようにサーバーをコールすることができます:

client.py

import shutil

import webapiclient


if __name__ == "__main__":
    service = webapiclient.Service("http://127.0.0.1:8008/api")
    service.usdprocessor.process_file(
        "input.usd", "surfacepoints", "output.usd")

    with service.usdprocessor.process_data(
            usd_data=webapiclient.File("input.usd"), hda_name="surfacepoints",
            ) as response_stream:
        with open("output_streamed.usd", "wb") as output_file:
            shutil.copyfileobj(response_stream, output_file)

完全なJavaScriptクライアント

例えば、上記のcall_api JavaScript関数を使用した場合、call_api("test.func1", {a: 1, b: "two"})と記述することができます。 以下のJavaScriptコードでは、service.test.func1({a: 1, b: "two"})と記述することで、もう少し自然体で関数をコールすることができます。 他にも、このクライアントは、例えばservice.test.binary_func({}, "arraybuffer")と記述してArrayBufferオブジェクトを取得することで、バイナリのレスポンスに対応します。

webapiclient.js

let _proxy_handler = {
    get: (obj, prop) => {
        let function_name = obj.function_name;
        if (function_name.length)
            function_name += ".";
        function_name += prop;

        new_obj = function (kwargs, response_type) {
            return call_api(function_name, kwargs, response_type);
        };
        new_obj.function_name = function_name;
        return new Proxy(new_obj, _proxy_handler);
    },
};

function call_api(function_name, kwargs, response_type)
{
    // response_typeは任意です。コール側は、バイナリデータを返すAPI関数をコールする時に、このresponse_typeに"arraybuffer"または
    // "blob"を設定することができます。
    if (kwargs === undefined)
        kwargs = {};

    var request = promisify_xhr(new XMLHttpRequest());
    request.open("POST", "/api", /*async=*/true);
    request.setRequestHeader(
        "Content-Type", "application/x-www-form-urlencoded");
    request.responseType =
        response_type !== undefined ? response_type : "json";
    return request.send("json=" + encodeURIComponent(
        JSON.stringify([function_name, [], kwargs])));
}

function promisify_xhr(xhr) {
    const actual_send = xhr.send;
    xhr.send = function() {
        const xhr_arguments = arguments;
        return new Promise(function (resolve, reject) {
            xhr.onload = function () {
                if (xhr.status != 200) {
                    reject({request: xhr});
                } else {
                    resolve(
                        xhr.responseType != "arraybuffer" &&
                            xhr.responseType != "blob"
                        ? xhr.response : JSON.parse(xhr.responseText));
                }
            };
            xhr.onerror = function () {
                // Pass an object with the request as a property.  This
                // makes it easy for catch blocks to distinguish errors
                // arising here from errors arising elsewhere.
                reject({request: xhr});
            };
            actual_send.apply(xhr, xhr_arguments);
        });
    };
    return xhr;
}

service = new Proxy({function_name: ""}, _proxy_handler);

プロトコルの仕様

以下の情報は、新しい言語でクライアントを実装する時に役立ちます。 参考にwebapiclient.pyを参照してください。

  • testというネームスペース内でabの引数を受け取るfunc1関数をコールすると仮定します。 aの値には、位置指定引数で1を渡し、bの値には、キーワード引数で“two”を渡します。

    • JSONエンコードの["test1.func", [1], {"b": "two"}]'["test.func1", [1], {"b": "two"}]'となります。

    • そのJSONエンコードの値をjsonという名前でURLエンコードすると次のようになります: json=%5B%22test.func1%22%2C+%5B1%5D%2C+%7B%22b%22%3A+%22two%22%7D%5D

    • これをサーバーの/apiURLパスにPOSTします。

  • POSTは推奨されるリクエストメソッドですが、他のメソッドも技術的に対応しています。 HTTPは結果をキャッシュ可能なものとして定義するので、副作用を持つ関数または引数以外のデータに依存する関数に対してGETを使用しないでください。

  • URIエンコーディングとmultipart/form-dataエンコーディングのどちらにも対応しています。

  • バイナリ引数を送信したり、または、大量の引数をストリーム化したいのであれば、 JSONエンコードされたデータにそれらの引数を含めないでください。 代わりに、それらの引数をmultipart/form-dataとしてボディ内にエンコードしてその引数名を使用し、Content-Typeにapplication/octet-streamを使用してください。 jsonという名前のバイナリ引数は対応していません。

  • サーバーがエラーメッセージをJSONとして返せるようにするために"application/json, */*"のAcceptヘッダを送信してください。

  • サーバーがapplication/octet-streamのContent-Typeを返す場合、API関数からのレスポンスはバイナリデータであってJSONではありません。

  • サーバーは、成功すれば200、APIError例外を引き起こせば422、制御されていない例外があれば500でレスポンスを返します。

  • API関数はリクエストに対してフルアクセスすることができるので、ヘッダを調べることができます。 この能力は、認証を実装する時に役立ちます。

  • API関数からのレスポンスを、HTTPステータスコード、Content-Type、追加ヘッダと一緒に返すことができます。 ただし、このプロトコルに準拠し、認証以外の独自の挙動を追加しないように心がけてください。

See also

hou.webServer

クラス

開始と停止

Webリクエストの処理とレスポンス

APIコール

  • hou.webServer.apiFunction()

    HoudiniのWebサーバー上のAPIエンドポイントを介してコール可能な関数のデコレータで、JSONレスポンスまたはバイナリレスポンスを返します。

  • hou.webServer.APIError

    apiFunctionハンドラーでこの例外を引き起こしてエラーを示します。