LoginSignup
1
2

More than 3 years have passed since last update.

Blue Prism で Object の仕様書を自動生成する

Last updated at Posted at 2020-09-24

はじめに

Blue Prism では、Object というアプリケーションを実際に操作する部品と、Process というビジネスロジックをかく部品のレイヤーが分かれていて、再利用性を高める仕組みになっています。
チーム内で Object の再利用を進めるには、「どういう Object があるのか」というドキュメントを作ってメンテナンスする必要がありますが、手で作ったりメンテナンスするのは辛かったり、Objectの内容がアップデートされているのに追従できなかったり、という問題が生じる可能性があります。

この記事では、そうした重要な Object に関するドキュメント(Blue Prism 用語でいうところの ODI : Object Design Instruction)を自動で生成しよう、という話を扱います。

「Object Inventory」 というVBOがある! が動かない。。。

Ditigal Exchange に 「Object Inventory」 という VBO があるのですが、文字列のトリムが英語前提になっていたり、謎の Excel エラー(Exception : HRESULT からの例外:0x800A03EC)が出たりして、うまく動きませんでした。。。

ただ、そのヘルプページに下記の記述がありました。

This asset is a business object that uses the output from the BP command line /getbod function to create a list of all business objects, pages, descriptions, inputs and outputs.

AutomateC.exe に /getbod というスイッチがあるということは、ヘルプのコマンドラインオプションにも書かれていないです。。。

/listprocesses と /getbod というスイッチ

先の「Object Inventory」VBOを覗いてみると、 /listprocesses スイッチで Process と Object の一覧を取得し、それぞれについて /getbod を呼んでいるようです。対象が Process だと、Could not find Business Object という文字列がかえってくるので、それらは処理対象から外す、という流れのようです。

/getbod スイッチは実在した!

python で実装してみる

拙いスクリプトですが、動作します。ご参考になれば幸いです。(Python 3.7.4で検証しました)

listprocesses したものについて、順次 getbod したものをテキストファイルに保存する

"""
Blue Prism に接続するためのパラメーターを BP_USERNAME, BP_PASSWORD, BP_DBCONNAME 環境変数に設定して実行してください。

BP_USERNAME : ユーザー名
BP_PASSWORD : パスワード
BP_DBCONNAME : 接続名
"""
import delegator
from pathlib import Path
import logging
import os

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

bp_username = os.environ["BP_USERNAME"]
bp_password = os.environ["BP_PASSWORD"]
bp_dbconname = os.environ["BP_DBCONNAME"]

LISTPROCESSES_CMD = '"C:\Program Files\Blue Prism Limited\Blue Prism Automate\AutomateC.exe" /user {bp_username} {bp_password} /dbconname {bp_dbconname} /listprocesses'
command = LISTPROCESSES_CMD.format(
    bp_username=bp_username, bp_password=bp_password, bp_dbconname=bp_dbconname
)
context = delegator.run(command)
object_list = context.out
object_names = object_list.splitlines()
logger.info(object_names)


GETBOD_CMD = '"C:\Program Files\Blue Prism Limited\Blue Prism Automate\AutomateC.exe" /user {bp_username} {bp_password} /dbconname {bp_dbconname} /getbod "{object_name}"'

for object_name in object_names:
    command = GETBOD_CMD.format(
        bp_username=bp_username,
        bp_password=bp_password,
        bp_dbconname=bp_dbconname,
        object_name=object_name,
    )
    context = delegator.run(command)
    description = context.out
    if (
        len(description.splitlines()) <= 1
    ):  # Process は説明が "XXというビジネスオブジェクトが見つかりませんでした" という1行だけ出力される
        logger.info("{} is not a object".format(object_name))
        continue
    # ファイル名に slash がはいっているとファイルを作成できなくなるので置換
    description_file_name = object_name.replace("/", "_") + ".txt"
    with open(Path("output_descriptions") / description_file_name, "w") as f:
        f.write(context.out)

getbod したテキストファイルを markdown にして保存する

もっと簡単にパースできるライブラリなどあったらどなたか教えてください。。。

from typing import List, Optional
from dataclasses import dataclass, field
import re
import enum
import logging
from pathlib import Path

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)


@dataclass
class VBOActionDescription:
    """
    VBO の各アクションの情報を保持するクラス
    """

    action_name: str
    description: str = ""
    pre_condition: str = ""
    post_condition: str = ""
    input_params: List[str] = field(default_factory=list)
    output_params: List[str] = field(default_factory=list)

    def as_markdown(self) -> str:
        """
        Markdown 形式で表現する
        """
        _md = (
            "###{action_name}\n"
            "{description}\n"
            "\n"
            "####事前条件\n"
            "{pre_condition}\n"
            "\n"
            "####事後条件\n"
            "{post_condition}\n"
            "\n"
            "####入力パラメーター\n"
            "{input_params}\n"
            "\n"
            "####出力パラメーター\n"
            "{output_params}\n"
            "\n"
        )
        input_params_md = ("\n").join(
            ["* {}".format(input_param) for input_param in self.input_params]
        )
        output_params_md = ("\n").join(
            ["* {}".format(output_param) for output_param in self.output_params]
        )

        out = _md.format(
            action_name=self.action_name,
            description=self.description,
            pre_condition=self.pre_condition,
            post_condition=self.post_condition,
            input_params=input_params_md,
            output_params=output_params_md,
        )
        return out


class VBODescription:
    """
    VBO の情報を保持するクラス。VBOActionDescription のリストを小として持つ
    """

    def __init__(
        self,
        object_name: str,
        description: Optional[str] = "",
        mode: str = "",
        actions: Optional[List[VBOActionDescription]] = None,
    ):
        self.object_name = object_name
        self.description = description
        self.mode = mode
        self.actions = actions

    def as_markdown(self) -> str:
        """
        Markdown 形式で表現する
        """
        _md = (
            "#{object_name}\n"
            "{description}\n"
            "\n"
            "##動作モード\n"
            "{mode}\n"
            "\n"
            "##アクション\n"
        )

        out = _md.format(
            object_name=self.object_name, description=self.description, mode=self.mode
        )
        if self.actions:
            out = out + ("\n").join([action.as_markdown() for action in self.actions])

        return out


class DescriptionOfWhat(enum.Enum):
    """
    VBO の説明文をパースする際に、「どの部分を読んでいるか」という情報が必要になったため設けた Enum
    """

    business_object = "Business Object"
    action = "Action"
    pre_condition = "Pre Conditiion"
    post_condition = "Post Conditiion"


def classify_line(line: str):
    """
    行の分類を行う
    """
    line = line.strip()
    # =ビジネスオブジェクト - Utility - File Management=  <=左のような行にマッチ
    match = re.search("^=(?P<content>[^=]*)=$", line)
    if match:
        return {"type": "object name", "content": match.groupdict()["content"]}
    # このビジネスオブジェクトの実行モードは「background」です  <=左のような行にマッチ
    match = re.search("^このビジネスオブジェクトの実行モードは「(?P<mode>[^」]+)」です", line)
    if match:
        return {"type": "mode", "content": match.groupdict()["mode"]}
    # ==Append to Text File==  <=左のような行にマッチ
    match = re.search("^==(?P<content>[^=]*)==$", line)
    if match:
        return {"type": "action name", "content": match.groupdict()["content"]}
    # ===前提条件===  <=左のような行にマッチ
    match = re.search("^===(?P<content>[^=]*)===$", line)
    if match:
        content = match.groupdict()["content"]
        if content == "前提条件":  # Blue Prism側の翻訳が変。。。
            return {"type": "pre condition", "content": content}
        if content == "エンドポイント":  # Blue Prism側の翻訳が変。。。
            return {"type": "post condition", "content": content}
        # それ以外
        return {"type": "action attribute", "content": content}
    # *入力:File Path (テキスト) - Full path to the file to get the file size   <=左のような行にマッチ
    match = re.search("^\*入力:(?P<content>.*)$", line)
    if match:
        return {"type": "input parameter", "content": match.groupdict()["content"]}
    # *出力:File Path (テキスト) - Full path to the file to get the file size  <=左のような行にマッチ
    match = re.search("^\*出力:(?P<content>.*)$", line)
    if match:
        return {"type": "output parameter", "content": match.groupdict()["content"]}
    # それ以外の行
    return {"type": "article", "content": line}


def append_action_to_vbo_description(latest_action, vbo_description):
    actions = vbo_description.actions
    if actions:
        vbo_description.actions.append(latest_action)
    else:
        vbo_description.actions = [
            latest_action,
        ]
    return vbo_description


def convert_to_markdown(bod_description_filepath) -> str:
    """
    Markdown に変換する処理の本体
    """
    vbo_description = None
    with open(bod_description_filepath, "r", encoding="shift_jis", newline="\r\n") as f:
        previous_line_type: Optional[DescriptionOfWhat] = None  # いまどの内容を読んでいるか、を保持する。
        latest_action = None

        for line in f:
            line_class = classify_line(line)
            if line_class["type"] == "object name":
                vbo_description = VBODescription(line_class["content"])
                previous_line_type = DescriptionOfWhat.business_object
                continue
            if line_class["type"] == "mode":
                assert vbo_description, "実行モードが正しい位置で記述されていません"
                vbo_description.mode = line_class["content"]
                continue
            if line_class["type"] == "article":
                assert vbo_description, "正しいフォーマットで記述されていません"
                if previous_line_type == DescriptionOfWhat.business_object:
                    vbo_description.description += line_class["content"]
                    continue
                if previous_line_type == DescriptionOfWhat.action:
                    assert latest_action, "正しいフォーマットで記述されていません"
                    latest_action.description += line_class["content"]
                    continue
                if previous_line_type == DescriptionOfWhat.pre_condition:
                    assert latest_action, "正しいフォーマットで記述されていません"
                    latest_action.pre_condition += line_class["content"]
                    continue
                if previous_line_type == DescriptionOfWhat.post_condition:
                    assert latest_action, "正しいフォーマットで記述されていません"
                    latest_action.post_condition += line_class["content"]
                    continue
            if line_class["type"] == "action name":
                assert vbo_description, "正しいフォーマットで記述されていません"
                if latest_action:
                    vbo_description = append_action_to_vbo_description(
                        latest_action, vbo_description
                    )
                latest_action = VBOActionDescription(line_class["content"])
                previous_line_type = DescriptionOfWhat.action
                continue
            if line_class["type"] == "input parameter":
                assert vbo_description and latest_action, "正しいフォーマットで記述されていません"
                latest_action.input_params.append(line_class["content"])
                continue
            if line_class["type"] == "output parameter":
                assert vbo_description and latest_action, "正しいフォーマットで記述されていません"
                latest_action.output_params.append(line_class["content"])
                continue
            if line_class["type"] == "pre condition":
                assert vbo_description and latest_action, "正しいフォーマットで記述されていません"
                previous_line_type = DescriptionOfWhat.pre_condition
                continue
            if line_class["type"] == "post condition":
                assert vbo_description and latest_action, "正しいフォーマットで記述されていません"
                previous_line_type = DescriptionOfWhat.post_condition
                continue
            # debug
            logger.debug("line: {}".format(line.strip()))
            if latest_action:
                logger.debug("latest_action: {}".format(latest_action.as_markdown()))
        else:
            # 最後に残っている latest_action を回収
            if latest_action:
                vbo_description = append_action_to_vbo_description(
                    latest_action, vbo_description
                )

    assert vbo_description, "正しいフォーマットで記述されていません"
    return vbo_description.as_markdown()


if __name__ == "__main__":
    descriptions_folder = Path("output_descriptions")
    for description_file in descriptions_folder.glob("*.txt"):
        with open(descriptions_folder / (description_file.stem + ".md"), "w") as md_f:
            md_f.write(convert_to_markdown(description_file))

参考URL

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2