On this page |
apiFunction(namespace=None, return_binary=False, arg_types=None, ports=[])
このデコレータを使って関数をデコレート(修飾)すると、その関数は/api
エンドポイントを介してコール可能な関数として登録されます。
-
Webサーバーの最も一般的な用途は、様々なサービスの接続に利用可能なAPIを構築することです。
-
このデコレータは、その関数を、RPC(Remote Procedure Call)システムを使用してコール可能な関数としてマークします。クライアント側のコードは、関数コール構文を使用してサーバーをコールすることができます。このデコレータは、REST形式のAPIsを生成しません。以下のコールする方法を参照してください。
Note
デコレータは、関数名をAPI関数名として使用します。
-
あなたの関数は必ずhou.webServer.Requestオブジェクトを1番目の引数として受け取らなければなりません。
この
Request
オブジェクトを使用することで、クライアント要求に関する情報(例えば、ヘッダ)を取得することができます。 -
あなたの関数は以下のどれかを返してください:
-
JSONにエンコード可能な値(
dict
,list
,str
,int
,float
,bool
,None
)。 -
バイナリデータ用の
bytearray
(Python3だとbytes
)オブジェクト。 -
hou.webServer.Responseオブジェクト(これは、hou.webServer.fileResponse()を使用すればファイルの内容をバイナリデータとしてストリーム化することができることを意味します)。
-
namespace
この文字列を指定すると、その文字列が関数名の接頭辞として作用します。
例えば、関数名がvector_length
でnamespace="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-Type
がapplication/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
というネームスペース内でa
とb
の引数を受け取る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
-
これをサーバーの
/api
URLパスに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 |