A certain engineer "COMPLEX"

.NETでPythonを試してみる 第5回

前回はIronPythonを捨てて、RESTを使ってPythonと連携しました。

Introduction


C#からPythonで公開したREST APIを呼び出すことには成功しました。
逆にC#で公開したREST APIを呼び出すことも可能です。
定期的にPython側での処理結果をC#側にプッシュするような形態のシステムもあり得ますので、PythonからC#への通信方法を知ることも有用でしょう。

今回のソースは下記になります

Sample source code for Demonstration, Experiment and Test - takuya-takeuchi/Demo

Python


requestsインストール

requestsは「人間のためのHTTP」と銘打った、極めてシンプルで洗練されたHTTPライブラリです。

インストールもお馴染みのpipで簡単。


$ python -m pip install requests
Collecting requests
Downloading requests-2.13.0-py2.py3-none-any.whl (584kB)
100% |################################| 593kB 1.1MB/s
Installing collected packages: requests
Successfully installed requests-2.13.0

以上でインストールは完了です。

ソース

前回のソースに手を加えた感じです。


from flask import Flask
from flask_restful import Resource, Api
import requests
from datetime import datetime as dt

app = Flask(__name__)
api = Api(app)

class DateTimeServiceClient:

def _send(self, datetime):
s = requests.session()

tstr = datetime.strftime("%Y/%m/%d %H:%M:%S.") + "%03d" % (datetime.microsecond // 1000)
params = {
'dateTime': tstr,
}
headers = {'content-type': 'application/json'}
r = s.post('http://localhost:5002/DateTimeService/Send', params=params, headers=headers)

class HelloWorld(Resource):
def get(self):
dtsc = DateTimeServiceClient();
tdatetime = dt.now()
dtsc._send(tdatetime);
return {'hello': 'world'}

api.add_resource(HelloWorld, '/')

if __name__ == '__main__':
app.run(debug=True)

getメソッド内で、DateTimeServiceClient._sendメソッドを呼び出します。
_sendメソッドは、外部のREST APIに対してPOSTで現在時刻を渡します。
つまり、外部(C#)から自身のREST APIを呼ばれている最中に、外部(C#)のREST APIを呼び出します。

重要なことですが、

  • headerspostメソッドに渡してください
  • paramsdateTimeキーの文字列を間違えないでください
  • postメソッドの第一引数はC#側ときちんと一致させること

これらが守られないと、クラッシュしたり、正しく動きません。
Python側は以上です。

C#


インストールするライブラリは前回と同じです。

ソース

WPFです。Xaml側は省略します。
Xamlは、Python側のREST APIを呼び出すButton、Python側から受け取る現在時刻情報を追加するListBoxです。
ViewModelもButtonに対応するコマンドと現在時刻のコレクションだけです。

まずは、公開するREST APIです。


using System;
using System.ServiceModel;

namespace WPFPython.Contracts.Service
{

[ServiceContract]
public interface IDateTimeService
{

event EventHandler<DateTime> DateTimeReceived;

[OperationContract]
void Send(string paramDateTime);

}

}

using System;
using System.ServiceModel;
using System.ServiceModel.Web;

namespace WPFPython.Contracts.Service
{

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public sealed class DateTimeService : IDateTimeService
{

#region イベント

public event EventHandler<DateTime> DateTimeReceived;

#endregion

#region メソッド

[WebInvoke(Method = "POST", UriTemplate = "Send?dateTime={paramDateTime}",
RequestFormat = WebMessageFormat.Json)]
public void Send(string paramDateTime)
{
var dt = DateTime.Parse(paramDateTime);
this.DateTimeReceived?.Invoke(this, dt);
}

#endregion

}

}

肝はSendメソッドに付与されたWebInvoke属性です。
プロパティは重要ですが、UriTemplateプロパティの値は特に重要です。
Sendメソッドの名前と、UriTemplateプロパティのSendは一致させる必要はありませんが、UriTemplateプロパティのSendはPython側で、postメソッドで指定した値と一致させます。
同様に、dateTimeもPython側のparamsのキーと一致させます。
ちなみに、UriTemplateプロパティのparamDateTimeの部分は、Sendメソッドの引数、paramDateTimeと一致させないと、アプリ起動時にクラッシュします。
Sendメソッドの中身は単純で、渡ってきた文字列をDateTime型に変換し、イベントを呼び出すだけです。

次にViewModelです。


using System;
using System.Collections.ObjectModel;
using System.Net.Http;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using WPFPython.Contracts.Service;
using WPFPython.ViewModels.Interfaces;

namespace WPFPython.ViewModels
{

public sealed class MainViewModel : ViewModelBase, IMainViewModel
{

#region フィールド

private readonly IDateTimeService _DateTimeService;

#endregion

#region コンストラクタ

public MainViewModel(IDateTimeService dateTimeService)
{
if (dateTimeService == null)
throw new ArgumentNullException(nameof(dateTimeService));

this._DateTimeService = dateTimeService;
this._DateTimeService.DateTimeReceived += (sender, time) =>
{
this._Responses.Insert(0, time.ToString("yyyy/MM/dd hh:mm:ss.fff"));
};

this._Responses = new ObservableCollection<string>();
}

#endregion

#region プロパティ

private RelayCommand _MessageCommand;

public RelayCommand MessageCommand
{
get
{
return this._MessageCommand ?? new RelayCommand(async () =>
{
var httpClient = new HttpClient();
await httpClient.GetStringAsync("http://127.0.0.1:5000/");
});
}
}

private readonly ObservableCollection<string> _Responses;

public ObservableCollection<string> Responses
{
get
{
return this._Responses;
}
}

#endregion

}

}

MessageCommandがPython側のREST APIを呼び出します。
また、その呼び出しで、C#側のREST APIがPython側から実行され、Python側から渡ってきた時刻がResponsesコレクションに追加されます。

最後にViewModelLocatorです。


/*
In App.xaml:
<Application.Resources>
<vm:ViewModelLocator xmlns:vm="clr-namespace:WPF1"
x:Key="Locator" />
</Application.Resources>

In the View:
DataContext="{Binding Source={StaticResource Locator}, Path=ViewModelName}"
*/

using System;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Windows;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Ioc;
using Microsoft.Practices.ServiceLocation;
using WPFPython.Contracts.Service;
using WPFPython.ViewModels;
using WPFPython.ViewModels.Interfaces;

namespace WPFPython
{
/// <summary>
/// This class contains static references to all the view models in the
/// application and provides an entry point for the bindings.
/// <para>
/// See http://www.galasoft.ch/mvvm
/// </para>
/// </summary>
public class ViewModelLocator
{

private static readonly ServiceHost _ServiceHost;

static ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

if (ViewModelBase.IsInDesignModeStatic)
{
// SimpleIoc.Default.Register<IDataService, Design.DesignDataService>();
}
else
{
// SimpleIoc.Default.Register<IDataService, DataService>();
}

var port = 5002;
var baseAddress = "http://" + Environment.MachineName + $":{port}/{typeof(DateTimeService).Name}";
var dateTimeService = new DateTimeService();
_ServiceHost = new WebServiceHost(dateTimeService, new Uri(baseAddress));
_ServiceHost.Open();

SimpleIoc.Default.Register<IDateTimeService>(() => dateTimeService);
SimpleIoc.Default.Register<IMainViewModel, MainViewModel>();

Application.Current.Exit += CurrentOnExit;
}

private static void CurrentOnExit(object sender, ExitEventArgs exitEventArgs)
{
try
{
_ServiceHost.Close();
}
catch (Exception e)
{
}
}

/// <summary>
/// Gets the Main property.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
"CA1822:MarkMembersAsStatic",
Justification = "This non-static member is needed for data binding purposes.")]
public IMainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<IMainViewModel>();
}
}
}
}

WebServiceHostインスタンス生成時のUrlに注意するのと、IDateTimeServiceに対応する実際の型は、MinaViewModelに保持するインスタンスと、WebServiceHostで保持するインスタンスは同一であることに注意します。
ポート番号も一致させます。
C#は以上です。

テスト


実際に実行してみます。
C#側を起動する前に、PythonでRESTサービス側を起動しておいてください。
ボタンを押下すると、Python側から受け取った現在時刻が画面下部のListBoxに追加されていきます。

Conclusion


C#側のRESTを活用して、C#とPythonで双方向通信が可能になりました。
実はRESTを使用したアプリを作るのは初めてでした。
WebInvoke属性の値の設定で苦労しましたが、良い経験になりました。

Source Code

https://github.com/takuya-takeuchi/Demo/tree/master/WPF.Python5

コメントを残す

メールアドレスが公開されることはありません。

%d人のブロガーが「いいね」をつけました。