PythonのgRPCクライアントを実装する(迷路のSVG生成)

以前作成した迷路生成サーバーをもとに迷路をSVGとして書き出すクライアントを書いてみた。 目的は久しぶりにPythonを触ること。 pipenvが標準な空気を醸し出してたので使ってみた。

ちなみに生み出されるSVGはこんな感じ。

迷路 迷路

これで簡単な迷路が作り放題だ。印刷して子供と遊べる。 魅力的にしたり仕掛けを追加したりは子供がクレヨンを持てるようになるまでに考える。

資料

記録

gRPCの準備

gRPCを利用するのに準備するのは3つ。

  • 仮想環境を準備
  • コード生成用の設定ファイルを書く
  • 生成されたコードが利用するimport文を有効にするためのグルーコードの設置

作業して生まれたディレクトリ配置はこんな感じ。 必要なら Makefile,Dockerfile やCI/CDな設定も置いたらおしまいって感じ。 mypy ちゃんとやろうとしたら生成されたコードでエラーになったので諦めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(svgmaze) $ tree .
.
|-- Pipfile
|-- Pipfile.lock
|-- codegen.py
`-- src
    |-- client.py
    |-- dummy_server.py
    `-- gen
        `-- pb
            |-- __init__.py
            |-- maze_pb2.py
            `-- maze_pb2_grpc.py
1
2
$ pipenv --python 3.8
$ pipenv install --dev grpcio-tools

コード生成用の設定ファイルを書く。 -I で指定したプレフィックス部分が proto ファイルパスから取り除かれて出力される。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from grpc.tools import protoc
protoc.main(
    (
        ''
        '-I../gen/grpc/maze/pb'
        '--python_out=./src/gen/pb'
        '--grpc_python_out=./src/gen/pb',
        '../gen/grpc/maze/pb/maze.proto',
    )
)

同モジュール内importを有効にするためのグルーコードを配置した。 これがないと生成された grpc.py 内の import 文が機能しない。

1
2
3
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent))

実装について

言語が変わってもだいたい同じなので、読めばわかる。

サーバーの時

  1. 生成された *_grpc.py で定義されてる *Servicer クラスを継承して実装する
  2. 実装した Servicer をserverに登録して開始

ref. dummy_server.py

クライアントの時

  1. grpcのチャンネルを開く
  2. チャンネルを元にスタブを生成する
  3. 呼び出す

リクエスト、レスポンスは *_pb2.py に定義されてる。

ref. client.py

SVGについて

Pythonで描画するのはsvgwriteがいまのところ落ち着いていそう。ライセンスはMITだしメンテナンスも継続されている。

SVGはxmlの拡張

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 1200 1200">
  <line x1="100" y1="100" x2="400" y2="100" stroke-width="10" stroke="#000000"/>
  <line x1="40" y1="30" x2="40" y2="450" stroke-width="0.5" stroke="#000000"/>
  <line x1="50" y1="30" x2="50" y2="450" stroke-width="1" stroke="#000000"/>
  <line x1="60" y1="30" x2="60" y2="450" stroke-width="2" stroke="#000000"/>
  <line x1="70" y1="30" x2="70" y2="450" stroke-width="4" stroke="#000000"/>
  <line x1="0" y1="1200" x2="1200" y2="0" stroke-width="4" stroke="#0000ff"/>
  <line x1="0" y1="0" x2="600" y2="600" stroke-width="4" stroke="#00ff00"/>
  <circle class="hoge" cx="150" cy="50" r="40" fill="blue"/>
</svg>

ルートノードのsvgの width,height が描画先を表していて viewBox が描画領域を指している。 子ノードとして描画する内容を色々と置いていけばいい。シンプルな迷路を書くだけならこの程度の理解でなんとかなる。

ライブラリとしてはキャンバスになる Drawing のインスタンスを生成して要素を追加して最後に save() するだけ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import svgwrite
import itertools

def _line(self, dwg, b_size, padding, start, end):
    return dwg.line(
        start=(start[0]*b_size/2 + padding, start[1]*b_size/2 + padding),
        end=(end[0]*b_size/2 + padding, end[1]*b_size/2 + padding))

dwg = svgwrite.Drawing(name, size=size, debug=True)
wall = dwg.add(dwg.g(id='wall', stroke='black'))
for w in itertools.chain(self.horizonWalls(), self.verticalWalls()):
    s, e = w[0], w[1]
    wall.add(self._line(dwg, block_size, padding, s, e))
dwg.save()

文字列で表現された迷路は、偶数行の奇数列が横壁、奇数行の偶数列が縦壁に対応するのでそれで線の座標を計算した。汚いけどこんな感じ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    ## ....
        self.fields = fields.split("\n")
    ## ....
    def verticalWalls(self) -> Iterator[Tuple[Tuple[int, int], Tuple[int, int]]]:
        header = self.fields[0]
        xl, yl = len(header), len(self.fields)
        for i in range(0, xl, 2): # for x
            last = 0
            for j in range(1, yl-1, 2):
                is_start = i == 0 and j == 1
                if is_start or self.fields[j][i] != '#':
                    if last != j-1:
                        yield ((i, last), (i, j-1))
                    last = j+1
            is_goal = i == xl-1
            if is_goal:
                yield ((i, last), (i, yl-4))
            else:
                yield ((i, last), (i, yl-2))

ref. https://github.com/masu-mi/genmaze/blob/master/svgmaze/src/maze/maze.py#L42-L69

comments powered by Disqus