Microsoft PurviewのLineageを外部に出力する（その3）

# この記事でやること
この記事ではMicrosoft PurviewのLineageをREST APIでJSON形式で取得しGraphvizを使って可視化します。
REST APIの実行はPythonを使っています。
※今回のカラム取得方法はイマイチな感じがしてます。より効率的なカラム取得方法を見つけたらアップデートします。（逆に効率的な方法をご存じの方がいたら教えてください！）

* [Microsoft PurviewのLineageを外部に出力する（その1）](https://qiita.com/cannoteat/items/1bc23d6b8c2d4ede1794)
* [Microsoft PurviewのLineageを外部に出力する（その2）](https://qiita.com/cannoteat/items/b4177ac7f150a6dd0d30)
* この記事

# Lineageの取得と表示
その1で取得したLineageをそのまま使っていきます。
今回はカラムの情報を追加します。

## カラムマッピングの取得
その1で各エンティティの情報をguidEntityMapから取得していますが、その中のresponse["guidEntityMap"][guid]["attributes"]["columnMapping"]にカラム毎のマッピング情報が格納されています。

DatasetMappingにSourceとSinkのqualifiedNameが格納されていて、columnMappingにSourceとSinkのカラム名が配列で格納されている感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89512/d6ed6f0a-7841-bc84-0a12-a5aff16928c1.png)


## Lineageとカラム情報をまとめる
Lineageを表示するための情報が多くなってきたのでGraphvizで使用する情報をentitiesにまとめています。
``` python
import pandas as pd

entities = {} # Graphvizで使用するデータの格納用
column_mappings = {}
pd.options.display.max_colwidth = 150
df_colum_mapping = pd.DataFrame(index=[], columns=["Source", "Sink"])

for guid in response["guidEntityMap"]:
    entities[guid] = {
        "guid": guid,
        "qualifiedName": response["guidEntityMap"][guid]["attributes"]["qualifiedName"],
        "name": response["guidEntityMap"][guid]["attributes"]["name"],
        "typeName": response["guidEntityMap"][guid]["typeName"],
        "classificationNames": response["guidEntityMap"][guid]["classificationNames"]
    }

    # columnMappingを持っているとき
    if response["guidEntityMap"][guid]["attributes"].get("columnMapping") is not None:
        # textで持っているのでJSONに変換
        json_column_mapping = json.loads(response["guidEntityMap"][guid]["attributes"]["columnMapping"])

        # SourceとSinkのqualifiedNameとカラム情報をDataFrameにまとめる
        for column_mapping in json_column_mapping:
            df_tmp = pd.json_normalize(column_mapping["ColumnMapping"])
            df_tmp = df_tmp.assign(SourceDataset=column_mapping["DatasetMapping"]["Source"])
            df_tmp = df_tmp.assign(SinkDataset=column_mapping["DatasetMapping"]["Sink"])
            df_colum_mapping = pd.concat([df_colum_mapping, df_tmp], ignore_index=True)

            # qualifiedNameを持っていないエンティティがあるため、GUIDから取得
            for relation in response["relations"]:
                if relation["toEntityId"] == guid:
                    if column_mapping["DatasetMapping"]["Source"] == "*":
                        df_colum_mapping.replace("*", response["guidEntityMap"][relation["fromEntityId"]]["attributes"]["qualifiedName"], inplace=True)

# SourceとSinkをまとめる
df_colum_mapping = pd.concat(
    [
        df_colum_mapping[["SourceDataset", "Source"]].rename(columns={"SourceDataset": "Dataset", "Source": "Column"}),
        df_colum_mapping[["SinkDataset", "Sink"]].rename(columns={"SinkDataset": "Dataset", "Sink": "Column"})
    ], 
    ignore_index=True)

# 重複排除
df_colum_mapping.drop_duplicates(inplace=True)
# display(df_colum_mapping)

# エンティティ毎にカラム情報を追加
for guid in entities:
    df_columns = df_colum_mapping.query(f'Dataset == "{entities[guid]["qualifiedName"]}"')
    df_columns = df_columns["Column"]
    entities[guid]["columns"] = df_columns.values.tolist()
```

## Lineageの表示（その3）

``` python
lineage03 = Digraph(format="svg")
lineage03.attr('graph', rankdir="LR", compound="true")
lineage03.attr("node", fontname="Segoe UI")

for guid in entities:
    lineage03.node(
        guid,
        shape="box",
        label=f'''
{guid}
{entities[guid]["name"]}
{entities[guid]["typeName"]}
{entities[guid]["columns"] if len(entities[guid]["columns"]) > 0 else ""}
        '''
    )

for relation in response["relations"]:
    lineage03.edge(relation["fromEntityId"], relation["toEntityId"])

lineage03.render("lineage/03.gv")
SVG("lineage/03.gv.svg")
```

こんな感じでカラム情報もLineageに追加できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89512/5c693fa3-d549-6c06-4a14-045054f48929.png)


# おまけ
ここからはGraphvizの話になります。
必要な情報は表示できましたが、モノクロで寂しいので色とかをつけて見やすくしていきます。
Graphvizではcssなども使えるようですが、今回使用しているノードのlabel情報には適用できないようなのでタグ内の要素にベタ打ちになっています。。

参考: Graphvizの[HTML-Like Labels](https://graphviz.org/doc/info/shapes.html#html)

Datasetの場合は四角に、Processの場合は角を丸くしたいのでentitiesに情報を加えておきます。（コードに書いてはいませんが、ここでアイコンのセットもしています。）
``` python
for guid in entities:
    # ProcessかDatasetかを設定
    if entities[guid]["typeName"].startswith("adf_"):
        entities[guid]["entityType"] = "process"
    else:
        entities[guid]["entityType"] = "dataset"
```

オリジナルのLineageには分類は表示されませんが情報としては持っているので、表示するようにしてみます。
``` python
lineage04 = Digraph(format="svg")
lineage04.attr("graph", rankdir="LR", compound="true")
# 背景色
lineage04.attr("graph", bgcolor="whitesmoke")
# ノードの枠線とフォントの設定
lineage04.attr("node", fontname="Segoe UI", fontcolor="#333333", color="dodgerblue")
# 矢印の色
lineage04.attr("edge", color="dimgray")

icon_dir = "icon/"
for guid in entities:
    entityInfo = ""

    # ADFのCopyアクティビティ、ADF名とPipeline名をセット
    if entities[guid]["typeName"] == "adf_copy_operation":
        entityInfo = f'''
    <tr>
        <td align="left"><font point-size="8">Data Factory:</font></td>
        <td align="left"><font point-size="8">{entities[guid]["qualifiedName"].split("/")[8]}</font></td></tr>
    <tr>
        <td align="left"><font point-size="8">Pipeline:</font></td>
        <td align="left"><font point-size="8">{entities[guid]["qualifiedName"].split("/")[10]}</font></td>
    </tr>
        '''
    # ADFのDataflow
    if entities[guid]["typeName"] == "adf_dataflow_operation":
        entityInfo = f'''
    <tr>
        <td align="left"><font point-size="8">Data Factory:</font></td>
        <td align="left"><font point-size="8">{entities[guid]["qualifiedName"].split("/")[-5]}</font></td></tr>
    <tr>
        <td align="left"><font point-size="8">Pipeline:</font></td>
        <td align="left"><font point-size="8">{entities[guid]["qualifiedName"].split("/")[-3]}</font></td>
    </tr>
        '''

    # SQL Databseのテーブルの場合は、Server名やDB名をセット
    if entities[guid]["typeName"] == "azure_sql_table":
        entityInfo = f'''
    <tr>
        <td align="left"><font point-size="8">Server:</font></td>
        <td align="left"><font point-size="8">{entities[guid]["qualifiedName"].split("/")[2]}</font></td>
    </tr>
    <tr>
        <td align="left"><font point-size="8">Database:</font></td>
        <td align="left"><font point-size="8">{entities[guid]["qualifiedName"].split("/")[3]}</font></td>
    </tr>
    <tr>
        <td align="left"><font point-size="8">Schema:</font></td>
        <td align="left"><font point-size="8">{entities[guid]["qualifiedName"].split("/")[4]}</font></td>
    </tr>
        '''

    # カラムをセット
    columns = ""
    for column in entities[guid]["columns"]:
        columns += f'''
    <tr><td align="left" colspan="3">    {column}</td></tr>
        '''

    # 分類をセット
    classifications = ""
    for classification in entities[guid]["classificationNames"]:
        classifications += f'''
    <font point-size="6" color="deeppink"><b>{classification.split(".")[-1]}</b></font>
        '''

    lineage04.node(
        guid,
        shape="plaintext",
        label=f'''<
<table border="1" cellspacing="1" cellborder="0" align="left"
    style="{"radial" if entities[guid]["entityType"] == "dataset" else "rounded"}"
    bgcolor="{"white" if entities[guid]["entityType"] == "dataset" else "aliceblue"}">
    <tr>
        <td rowspan="2" fixedsize="true" width="20" height="20"><img src="{icon_dir + entities[guid]["icon"]}" scale="true"/></td>
        <td align="left" colspan="2" border="0"><b>{entities[guid]["name"]}</b></td>
    </tr>
    <tr>
        <td colspan="2" align="left"><b><font point-size="10">{entities[guid]["typeName"]}</font></b></td>
    </tr>
    {entityInfo}
    <tr><td colspan="2">{classifications}</td></tr>
    {columns}
</table>
        >'''
    )

for relation in response["relations"]:
    lineage04.edge(relation["fromEntityId"], relation["toEntityId"])

lineage04.render("lineage/04.gv")
SVG("lineage/04.gv.svg")
```
### 結果
こんな感じになりました。
多少は見やすくなったでしょうか。。エンティティのtypeName毎に設定やアイコンを変えたりと汎用性のない作りにはなってしまいましたが、Lineageの出力自体はできました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89512/672ed50b-6a84-4d0a-798c-fc8badf005b9.png)


# 
* [Microsoft PurviewのLineageを外部に出力する（その1）](https://qiita.com/cannoteat/items/1bc23d6b8c2d4ede1794)
* [Microsoft PurviewのLineageを外部に出力する（その2）](https://qiita.com/cannoteat/items/b4177ac7f150a6dd0d30)
* この記事