On this page |
apiFunction(namespace=None, return_binary=False, arg_types=None, ports=[])
このデコレータで関数をデコレートすると、その関数は、/api
エンドポイント経由でコール可能な関数として登録されます。
-
ウェブサーバーで最も頻度の高い用途は、様々なサービスを繋げる時に使用できるAPIを構築することです。
-
このデコレータは、関数がリモートプロシージャルコール(RPC)システムを使用してコール可能であることを示します。クライアント側のコードは、関数コール構文を使用してサーバーをコールすることができます。このデコレータはREST形式のAPIを生成しません。以下のコールする方法を参照してください。
Note
このデコレータは、関数名をAPI関数名として使用します。
-
あなたの関数は、必ず1番目の引数にhwebserver.Requestオブジェクトを受け取らなければなりません。
その
Request
オブジェクトを使用して、ヘッダなどのクライアントリクエストに関する情報を取得することができます。 -
あなたの関数は、以下のどれかを返すようにしてください:
-
JSONにエンコード可能な値(
dict
,list
,str
,int
,float
,bool
,None
)。 -
バイナリデータの場合は
bytearray
(またはPython3ならbytes
)オブジェクト。 -
hwebserver.Responseオブジェクト(これは、hwebserver.fileResponseを使用してファイルの内容をバイナリデータとしてストリームさせることができることを意味します)。
-
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-Type
がapplication/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
ネームスペース内にあるa
とb
の引数を受け取る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 |