Help us understand the problem. What is going on with this article?

pythonでバッチスクリプトを書くときの雛形

仕事上Pythonでスクリプトをよく書くので、雛形コードを備忘録も兼ねて載せておきます。

python3系の雛形

概要

この雛形では以下のことをしています。

  • コマンドライン引数のパース(clickの利用)
  • 設定クラスの読み込み
  • ログ出力(loggingの利用)
  • ライブラリ読み込み
  • ライブラリの単体テスト

ファイルの配置

app_home/
       ├ bin/
       │   └  my_batch.py   #←実行するスクリプト
       ├ conf/
       │   └  my_batch_conf.py #←設定クラス
       ├ lib/
       │   ├  __init__.py    #←モジュールをロードするのに必要
       │   └  my_lib.py      #←ライブラリ
       ├ tests/        
       │   └  test_my_lib.py #←単体テストコード
       ├ log/                #←ログ出力先
       └ Pipfile             #←使うライブラリを列挙

内容

本体my_batch.pyの内容

import logging
import os
import sys
from pathlib import Path

import click

# 親ディレクトリをアプリケーションのホーム(${app_home})に設定
app_home = str(Path(__file__).parents[1])
# ${app_home}をライブラリロードパスに追加
sys.path.append(app_home)

# 自前のライブラリをロード
from lib.my_lib import MyLib

# 設定クラスのロード
from conf.my_batch_conf import MyBatchConf


# コマンドライン引数のハンドリング. must_argは必須オプション、optional_argは任意オプション
@click.command()
@click.option('--must_arg', '-m', required=True)
@click.option('--optional_arg', '-o', default="DefaultValue")
def cmd(must_arg, optional_arg):
    # 自身の名前から拡張子を除いてプログラム名(${prog_name})にする
    prog_name = os.path.splitext(os.path.basename(__file__))[0]

    # ロガーの設定

    # フォーマット
    log_format = logging.Formatter("%(asctime)s [%(levelname)8s] %(message)s")
    # レベル
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    # 標準出力へのハンドラ
    stdout_handler = logging.StreamHandler(sys.stdout)
    stdout_handler.setFormatter(log_format)
    logger.addHandler(stdout_handler)
    # ログファイルへのハンドラ
    file_handler = logging.FileHandler(os.path.join(app_home, "log", prog_name + ".log"), "a+")
    file_handler.setFormatter(log_format)
    logger.addHandler(file_handler)

    # 処理開始
    try:
        # ログ出力
        logger.info("start")

        # コマンドライン引数の利用
        logger.error(f"must_arg = {must_arg}")
        logger.error(f"optional_arg = {optional_arg}")

        # ライブラリ呼び出し
        mylib = MyLib()
        logger.info(mylib.get_name())

        # 設定値の利用
        logger.info(MyBatchConf.key1)
        logger.info(MyBatchConf.key2)

        # 例外が発生しても・・・
        raise Exception("My Exception")

    except Exception as e:
        # キャッチして例外をログに記録
        logger.exception(e)
        sys.exit(1)


if __name__ == '__main__':
    cmd()

設定クラスconf/my_batch_conf.pyの内容

class MyBatchConf(object):
    key1 = "key1_value"
    key2 = True

※)以前はconfigParserを利用していましたが、設定クラスの方がパースする必要が無いですし、IDEによる補完が効くため、今は利用しなくなりました。

ライブラリmy_lib.pyの内容

class MyLib(object):
    def get_name(self):
        return "my_lib"

ライブラリの単体テストコードtest_my_lib.pyの内容

import sys,os
import unittest

# ../libをロードパスに入れる
app_home = os.path.abspath(os.path.join( os.path.dirname(os.path.abspath(__file__)) , ".." ))
sys.path.append(os.path.join(app_home,"lib"))

# ../テスト対象のライブラリのロード
from my_lib import MyLib

class TestMyLib(unittest.TestCase):

    def test_get_name(self):
        ml = MyLib()
        self.assertEqual("my_lib", ml.get_name())

if __name__ == '__main__':
    unittest.main()

実行

引数を指定しないで実行

$ python bin/my_batch.py

実行結果(clickの機能によりマニュアルが出る)

Usage: my_batch.py [OPTIONS]
Try "my_batch.py --help" for help.

Error: Missing option "--must_arg" / "-m".

引数を指定して実行

$ python bin/my_batch.py -m SpecifiedValue

実行結果

2019-06-28 16:42:53,335 [    INFO] start
2019-06-28 16:42:53,335 [   ERROR] must_arg = SpecifiedValue
2019-06-28 16:42:53,335 [   ERROR] optional_arg = DefaultValue
2019-06-28 16:42:53,335 [    INFO] my_lib
2019-06-28 16:42:53,336 [    INFO] key1_value
2019-06-28 16:42:53,336 [    INFO] True
2019-06-28 16:42:53,336 [   ERROR] My Exception
Traceback (most recent call last):
  File "bin/my_batch.py", line 62, in cmd
    raise Exception("My Exception")
Exception: My Exception

ログ(log/my_batch.log)にも同じ内容が出力される

2019-06-28 16:42:53,335 [    INFO] start
2019-06-28 16:42:53,335 [   ERROR] must_arg = SpecifiedValue
2019-06-28 16:42:53,335 [   ERROR] optional_arg = DefaultValue
2019-06-28 16:42:53,335 [    INFO] my_lib
2019-06-28 16:42:53,336 [    INFO] key1_value
2019-06-28 16:42:53,336 [    INFO] True
2019-06-28 16:42:53,336 [   ERROR] My Exception
Traceback (most recent call last):
  File "bin/my_batch.py", line 62, in cmd
    raise Exception("My Exception")
Exception: My Exception

テスト

単体テストの実行

bash-3.2$ python tests/test_my_lib.py

実行結果

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

tests以下の全テストコードtest_*.pyをまとめて実行する

$ python -m unittest  discover tests "test_*.py"

最後に

このコードはgithubでも公開しています→ https://github.com/fetaro/python-batch-template-for-v3

python2系の雛形

こちらに移動しました

fetaro
データエンジニア
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away