環境毎の設定ファイルを管理・出力

Python勉強中!何か作りたいものがないかと思い、アプリのリリースで環境毎に設定値に差分があり、毎回手作業で確認しながらやるのがつまらなすぎてしんどい、ということでbottleのSimpleTemplateを使って環境毎の設定ファイルを生成できないかやってみた。

下記のソースはyamlを使うので、lib_yamlにruamelをpipでインストールする必要がある。なぜこうしたのかはJAVAと違って、Pythonはwindowsで作ったものをzipで固めて、そのままLinux(インターネット接続なし)で展開して使うことができなそうだから。

bottle.pyもダウンロードしておく必要あり。

変更履歴
[v02]
・bottleのtemplate()ではshift_jisでエラーとなってしまうため、引数で色々指定できるSimpleTemplateに変更。
・config.ymlでテンプレートファイルに対してencodingをオプションで指定できるように変更。指定する値はshift_jis、utf-8など。
[v03]
・以下のようにxml: “on”を指定した場合、xml部分をフォーマットで改行した形でtemplateを書き換えるように対応。

     val: “<note><to>a</to><from>b</from><heading>no title</heading><body>test</body></note>”
xml: “on”

構成

# tree
├── bottle.py
├── config.yml
├── createConfigTile.py
├── lib_yaml
├── log
├── loggerConfig.yml
└── template
    └── appA
        ├── config1.txt
        └── config2.txt

from bottle import SimpleTemplate, template
import os
import sys
from logging import Logger, getLogger, config
from typing import Dict
#import xml.dom.minidom
import xml.etree.ElementTree as ET
import re

class const:
    class ConstError(TypeError):
        pass

    def __setattr__(self, name, value):
        if name in self.__dict__:
            raise self.ConstError("Can't rebind const (%s)" % name)
        self.__dict__[name] = value

class Main:
    def __init__(self):
        # 相対パスでも動くよう、カレントディレクトリを変更する
        os.chdir(os.path.dirname(os.path.abspath(__file__)))
        # PYTHONPATHにzipファイルを追加
        basepath = os.path.split(os.path.realpath(__file__))[0]
        sys.path.insert(0, os.path.join(basepath, 'lib_yaml/'))

        import ruamel.yaml
        import ruamel

        yaml = ruamel.yaml.YAML()

        with open('./loggerConfig.yml') as stream:
            logconfig = yaml.load(stream)

        os.makedirs("./log/", exist_ok=True)
        config.dictConfig(logconfig)

        self.logger = getLogger(self.__class__.__name__)
        self.logger.info("Main class init")

    def exe(self):
        self.logger.info("execute exe()")
        self.yyy = ConfigCreate()
        self.yyy.exe()


class ConfigCreate:

    C_ = const()
    C_.NEWLINE_CRLF = 0
    C_.NEWLINE_LF = 1
    C_.NEWLINE_CR = 2
    C_.NEWLINE_UNKNOWN = -1
    BB=3

    def __init__(self):
        self.logger = getLogger(self.__class__.__name__)
        self.logger.info("ConfigCreate class init")

    def gainkaiGyo(self, filepath : str, encoding : str) -> int:

        kcrlf = '\r\n'
        klf = '^([^\r]*)\n'
        kcr = '\r(?!\n)'

        re_crlf = re.compile(kcrlf)
        re_lf = re.compile(klf)
        re_cr = re.compile(kcr)

        with open(filepath, mode='r', encoding=encoding, newline='') as fco:
            data = fco.readline()

            while data:
                #print("content:" + data)
                if re_crlf.search(data) is not None:
                    #print("crlf")
                    return self.C_.NEWLINE_CRLF #crlf
                elif re_lf.search(data) is not None:
                    #print("lf")
                    return self.C_.NEWLINE_LF #lf
                elif re_cr.search(data) is not None:
                    #print("cr")
                    return self.C_.NEWLINE_CR #cr
                else:
                    #print("不明")
                    return self.C_.NEWLINE_UNKNOWN #不明

            return -1  #不明 

    def gainTemplateVal(self, ooo: dict, new_line: int) -> dict:
        """
        bottleライブラリのテンプレートに渡すパラメータを作成する。
        以下のようにxml: "on"設定がある場合は、引数(new_line)に従って改行を付与する
        var:
           x1: 
             val: "<note><to>a</to><from>b</from><heading>no title</heading><body>test</body></note>"
             xml: "on"
        """

        if new_line == self.C_.NEWLINE_CRLF :
            self.logger.debug( '改行=>CRLF' )
        elif new_line == self.C_.NEWLINE_LF :
            self.logger.debug( '改行=>LF' )
        elif new_line == self.C_.NEWLINE_CR :
            self.logger.debug( '改行=>CR' )
        elif new_line == self.C_.NEWLINE_UNKNOWN :
            self.logger.debug( '改行=>不明' )

        templateVal: Dict = {}
        for kk, vv in ooo.items():
            if isinstance(vv, dict):
                self.logger.info("dict =========")
                self.logger.info(vv)
                for key, vvv in vv.items():
                    self.logger.info(key + "," + vvv)
                    #  templateVal[key] = vvv

                if "val" in vv and "xml" in vv:
                    if vv['xml'] == 'on':
                        #dom = xml.dom.minidom.parseString(vv['val'])
                        #templateVal[kk] = dom.toxml()

                        ooo = ET.fromstring(vv['val'])
                        ET.indent(ooo)
                        #改行を一旦\nに統一
                        templateVal[kk] = ET.tostring(element=ooo, encoding='unicode').replace('\r\n', '\n').replace('\r', '\n')
                        if new_line == self.C_.NEWLINE_CRLF :
                            templateVal[kk] = templateVal[kk].replace('\n', '\r\n')
                        elif new_line == self.C_.NEWLINE_LF :
                            #上の処理で\nにしたので実施する必要ないけど一応実施
                            templateVal[kk] = templateVal[kk].replace('\n', '\n')
                        elif new_line == self.C_.NEWLINE_CR :
                            templateVal[kk] = templateVal[kk].replace('\n', '\r')
                        elif new_line == self.C_.NEWLINE_UNKNOWN :
                            #NEWLINE_UNKNOWNの場合は改行をLFにする
                            templateVal[kk] = templateVal[kk].replace('\n', '\r')

            else:
                templateVal[kk] = vv

        return templateVal


    def exe(self):
        import ruamel.yaml
        import ruamel
        yaml = ruamel.yaml.YAML()


        with open('./config.yml') as stream:
            data = yaml.load(stream)


        for file_ in data['files']:
            self.logger.info('-----------------------------------------------')
            self.logger.info('テンプレートファイル名:' + file_['filename'])

            #デフォルトエンコード:utf-8
            enc = "utf-8"
            if "encoding" in file_:
                self.logger.info('encoding:' + file_['encoding'])
                enc = file_['encoding']


            for kankyo in file_['kankyo']:
                self.logger.info(kankyo)
                for app in kankyo['apps']:
                    self.logger.info("---設定ファイル作成開始 " + "環境=" + kankyo['kankyoname'] + ", アプリ=" + app['appname'] + "---")
                    self.logger.info("設定=>" + str(app))

                    #作成する設定ファイルのディレクトリを決定する
                    index = file_['filename'].find('./template/')
                    inifile_excludePathTemplate = file_['filename'][slice(index + len('./template/'), len(file_['filename']))]
                    templateFolderPath = os.path.dirname(inifile_excludePathTemplate)
                    self.logger.info("templateFolderPath:" +  templateFolderPath)

                    folderpath = './app_config/' + kankyo['kankyoname'] + "/" + app['appname'] + "/" + templateFolderPath
                    self.logger.info("出力フォルダ:" + folderpath)
                    os.makedirs(folderpath, exist_ok=True)

                    #設定ファイルのファイル名を取得
                    basename = os.path.basename(inifile_excludePathTemplate)

                    template_newline = self.gainkaiGyo(file_['filename'], enc)
                    templateVal: Dict = self.gainTemplateVal(app['var'], template_newline)
                    self.logger.info(templateVal)

                    self.logger.info("テンプレートファイルより設定ファイル作成実施")
                    with open(folderpath + "/" + basename, mode='w', encoding=enc, newline='') as f:
                        with open(file_['filename'], mode='r', encoding=enc, newline='') as fco:
                            tpl = SimpleTemplate(source=fco)
                            f.write( tpl.render(**templateVal) )
                            f.close()
                            self.logger.info("---設定ファイル作成完了---")

if __name__ == '__main__':
    testClass2 = Main()
    testClass2.exe()
files:
 - filename: "./template/appA/config1.txt"
   kankyo:
     - kankyoname: "kankyo1"
       apps:
       - appname: "appA1"
         var:
           x1: "aiueo1"
           x2: "ueo1"
       - appname: "appA2"
         var:
           x1: "aiueo2"
           x2: "ueo2"
     - kankyoname: "kankyo2"
       apps:
       - appname: "appA1"
         var:
           x1: "aiueo3"
           x2: "ueo3"
       - appname: "appA2"
         var:
           x1: "aiueo4"
           x2: "ueo4"
 - filename: "./template/appA/config2.txt"
   #オプションでencodingを指定可能
   encoding: "shift_jis"
   kankyo:
     - kankyoname: "kankyo1"
       apps:
       - appname: "appA1"
         var:
           x1: "aiueo5"
           x2: "ueo5"
       - appname: "appA2"
         var:
           x1: "aiueo6"
           x2: "ueo6"
     - kankyoname: "kankyo2"
       apps:
       - appname: "appA1"
         var:
           x1: 
             val: "<note><to>a</to><from>b</from><heading>no title</heading><body>test</body></note>"
             xml: "on"
           x2: "ueo7"
       - appname: "appA2"
         var:
           x1: "aiueo8"
           x2: "ueo8"
aaaa.path={{x1}}
bbbbb.count={{x2}}