はじめに
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))