Houdini 20.0 hwebserver

hwebserver.apiFunction function

Houdiniウェブサーバー上のAPIエンドポイント経由で関数をコールを可能にするデコレータで、JSON形式またはバイナリ形式のレスポンスを返します。

On this page

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

このデコレータで関数をデコレートすると、その関数は、/apiエンドポイント経由でコール可能な関数として登録されます。

Note

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

namespace

ここに文字列を指定すると、それが関数名の接頭辞として機能します。 例えば、関数名がvector_lengthで、namespace="geo"を引数として渡すと、ネームスペース付きの関数名は"geo.vector_length"となります。 あなたのAPI関数をネームスペース内にまとめることを推奨します。

return_binary

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

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

arg_types

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

ports

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

サーバーは、クライアントから指定されたタイプの引数をコールした後に、それらの引数でその関数をコールします。 これは、クライアントが引数を文字列として指定した場合に役立ちます(以下のコールする方法のAPIをコールする1つ目の方法を参照してください)。

import hou
import hwebserver


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

Note

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

NEW

API関数はasyncioに対応しています(Python3ビルド版のみ)。 asyncioを使用するとパフォーマンスが低下するので、ウェブサーバーはC++が良いです。 APIがリクエスト処理中にIOを待つことが多い場合にのみ、コルーチンAPI関数を使用するのを推奨します。 デコレータは、自動的に関数の型を検出し、その型に基づいてその関数を処理します。

コールする方法

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

  • 1つ目の方法は、cmd_functionパラメータに関数名を、その他のすべてのパラメータに引数を格納してapiにPOSTする方法です。例えば、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つ目の方法は、jsonパラメータに(関数名の文字列、位置指定引数のリスト、キーワード引数オブジェクトが含まれた)JSONオブジェクトの文字列エンコードを格納してapiにPOSTする方法です。例えば、["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クライアントのサンプル。

    # サーバーコード:
    @hwebserver.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での引数の型の指定方法

    @hwebserver.apiFunction("test", arg_types={"value": int})
    def double_int(value):
        return value * 2
    
    # "value=3"のPOSTボディでコールすると、その文字列の"3"が整数の3に変換されます。
    

    Python3では、引数の型も型ヒントを使って指定することができます。

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

バイナリデータ

  • API関数は、bytearrayオブジェクト(Python3ではbytesオブジェクト)を返すことで、または、Content-Typeapplication/octet-streamのレスポンスを返すことでバイナリデータを返すことができ、hwebserver.fileResponseを使用してapplication/octet-streamによってファイルの内容をストリームしたり、 関数がバイナリを返すものと示してbytesを返すことができます。

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

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

    # サーバーは、以下のようにしてバイナリデータを返すことができます:
    
    @hwebserver.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関数がバイナリを返すように示したサーバー

    @hwebserver.apiFunction("test", return_binary=True)
    def binary_data(request):
        return b'\x00\x00AB\x00'
    
  • webapiclient.pyを使用すると、bytearrayオブジェクトを使って引数にバイナリデータを渡したり、webapiclient.Fileオブジェクトを使ってディスク上のファイルからバイナリデータをストリームさせることができます。

    Note

    バイナリデータ引数はキーワード引数を使用して渡す必要があり、位置指定引数で渡すことはできません。

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

    # クライアントは、以下のようにしてバイナリデータを送信することができます:
    service.test.use_binary_data(
        small_data=bytearray([0, 0, 65, 66, 0]),
        big_data=webapiclient.File("/path/to/binary/file"))
    
    
    # サーバーは、バイナリデータをUploadedFileオブジェクトとして受信して、
    # そのオブジェクトからバイナリデータを読み込みます:
    import shutil
    import hwebserver
    
    @hwebserver.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)
    

エラー

  • API関数でhwebserver.APIErrorが引き起こされると422ステータスが返されます。

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

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

    エラーについて説明したServerコードとPythonクライアントコード

    # サーバー:
    
    @hwebserver.apiFunction("test")
    def illustrate_errors(request, value):
        if value == 0:
            return ["successful", "result"]
        if value == 1:
            raise hwebserver.APIError("an error occurred")
        if value == 2:
            raise hwebserver.APIError({"any": "json-encodable"})
        if value == 3:
            raise hwebserver.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がプリントされます。
    

サンプル

関連ファイルの用意

時折、ハンドラーを含んだPythonスクリプトファイル関連のパスを使用してファイルを用意したい場合があります。 通常では、__file__変数には、現在実行中のファイルのパスが入るので、os.path.dirname(__file__)を使用することで、そのファイルが含まれているディレクトリを取得することができます。

しかし、コマンドラインからPythonスクリプトを使ってHoudiniを起動した場合などでは、__file__は定義されていません。 これを補足するには、以下の関数を使用すると良いでしょう:

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)

USDを処理するサービス

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

このサンプルでは2つのAPI関数を用意しています。 1つ目のAPI関数では、クライアントとサーバーがどちらも同じファイルサーバーから入力/出力USDデータファイルにアクセスすることができ、 クライアントが入力USDファイルの場所とUSDファイルの出力先を指定することが前提になっています。 2つ目のAPI関数は、実際にファイルの内容を回送し、クライアントからの入力ファイルをストリームさせるので、クライアントのメモリを占有しません。 サーバーは、入力ファイルをhwebserver.UploadedFileとして受信するので、メモリが占有されず、ファイルの内容がディスク上にまだ存在しなければその内容がディスクに保存されるので、Houdiniはその内容を読み込んで、1つ目のAPI関数と同様にUSDファイルを処理し、一時的なファイル出力をストリームします。

usdprocessor.py

import os
import tempfile

import hou
import hwebserver

import webutils


@hwebserver.apiFunction("usdprocessor")
def process_file(request, input_file, hda_name, output_file):
    '''
    サーバーからアクセス可能な入力USDファイルのパスを与えると、そのUSDファイルが読み込まれ、
    そのサーバー上に保存されているSOP HDAの名前を使ってそのUSDファイルを処理し、
    クライアントからアクセス可能なファイルの場所にその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 hwebserver.APIError(e.instanceMessage())

    # 入力USDファイルを読み込んで、SOPアセットを使用してそのUSDファイルを処理し、
    # 新しいファイルを保存するLOPネットワークを構築します。
    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 hwebserver.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 hwebserver.OperationFailed("This asset is not a SOP")
    return definition.nodeTypeName()


@hwebserver.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 hwebserver.fileResponse(
        temp_output_file, content_type="application/octet-stream",
        delete_file=True)


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

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

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"})のように記述することで少し自然な感じでAPI関数をコールすることができます。 このクライアントは、例えば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 () {
                // リクエストをプロパティとしてオブジェクトを渡します。
                // これによって、catchブロックはここで発生したエラーと別のところで発生したエラーを
                // 区別しやすくなります。
                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”を渡すとします。

    • '["test.func1", [1], {"b": "two"}]'を取得するJSONエンコードは["test1.func", [1], {"b": "two"}]です。

    • そのJSONエンコード値をURLエンコードしたものをjsonという名前に入れます:json=%5B%22test.func1%22%2C+%5B1%5D%2C+%7B%22b%22%3A+%22two%22%7D%5D

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

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

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

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

  • "application/json, */*"のAcceptヘッダを送信すると、サーバーはエラーメッセージをJSONとして返します。

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

  • サーバーは、成功すると200、APIError例外を引き起こすと422、未処理の例外があると500で応答します。

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

  • API関数からのレスポンスにHTTPステータスコード、Content-Type、さらにヘッダを加えて返すことができます。このプロトコルを確認することを推奨しますが、認証以外の独自のカスタム動作を追加しないでください。

See also

hwebserver

クラス

  • hwebserver.Request

    Houdiniのウェブサーバーに送信されるリクエスト。

  • hwebserver.Response

    Houdiniのウェブサーバーから送り返されるレスポンス。

  • hwebserver.UploadedFile

    Houdiniのウェブサーバーに送信されるリクエストにアップロードされたファイル。

  • URLHandler

    汎用のHTTPハンドラー。

  • AsyncURLHandler

    汎用の非同期HTTPハンドラー。

  • WebSocket

    組み込みサーバーをWebSocketに対応させるための基底クラス。

開始と停止

  • hwebserver.run

    Houdiniのウェブサーバーを開始します。

  • hwebserver.requestShutdown

    すべてのオープンリクエストが処理された後にシャットダウンするようにHoudiniのウェブサーバーに命令します。

  • hwebserver.isInDebugMode

    Houdiniのウェブサーバーがデバッグモード(でdebug=True)で起動されていればTrueを返します。

ウェブリクエストの処理とレスポンスの返し

ウェブソケット

  • WebSocket

    組み込みサーバーをWebSocketに対応させるための基底クラス。

  • hwebserver.webSocket

    HoudiniのウェブサーバーにWebSocketクラスを登録するデコレータ。

APIコール

  • hwebserver.apiFunction

    Houdiniウェブサーバー上のAPIエンドポイント経由で関数をコールを可能にするデコレータで、JSON形式またはバイナリ形式のレスポンスを返します。

  • hwebserver.APIError

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