Flaskをベースにフレームワーク作ってみた

# この記事は？
Webページを制作するにあたり、flaskを使うことがままありますが、簡単な機能については共通化したりできないかなと考えてFlaskを少し拡張したという意味でBeakerと名付けてWebページを簡単に作れるような機能を提供できないかと考えました。
現時点では作成中ですが、本当に簡単な画面であれば作れるようになってきたので現段階でご紹介したいなと思います。
今後の拡張は随時行っていく予定なので今の機能がそのまま残っている保証はないのですが、そういうものだと割り切って見ていただけたらと思います。また、使ってみたいなという人がいれば自由にforkいただければと思います！

* 実際に作っているものは下記にあります

https://github.com/KiharaTakahiro/beaker

# どんなことができるの？
* web.pyによるルーティングの一元管理
* POSTの場合はCSRFトークンが必要になる設定
* ログ出力機能
* セッションの使用
* DBとの接続およびトランザクション機能
* CSV出力機能
* カスタムフィルタの使用

## web.pyによるルーティングの一元管理
ルーティングはLaravelを少しイメージした[web.py](https://github.com/KiharaTakahiro/beaker/blob/main/web.py)に記載する方式をとりました。Flaskではアノテーションを使用したルート定義ですが、やはりルーティングには一覧性があるLaravelのweb.phpのような書き方の方がいいのではないか？と考えてこの方式をとっています。もちろん大規模開発においてはFlaskのようなアノテーションを使用する方式が良いと私も考えているものの軽くサクッと作りたいと考えている場合はこの方式の方が一覧として見やすい気もしますし、何よりLaravelから入ってきたエンジニアはこっちの方が違和感なく使えるんでは？と思います。
(いろいろそれっぽいことを書いたもののFlaskがアノテーション使ってるからなんとなくLaravelみたいに書けないかと思って遊んだ結果こうなったのが本音です)

具体的な書き方は以下のような方法となります。
### [web.py](https://github.com/KiharaTakahiro/beaker/blob/main/web.py)にてrouterの設定を行う
```python: web.py
# routerのインポート
from common.beaker import BeakerRouter
router = BeakerRouter()

from controllers.welcome_controller import welcome, welcome_post

# ルート定義の設定
# GETさせたい場合はrouter.get(endpoint, function)となるように記載する
router.get('/', welcome)
# POSTさせたい場合はrouter.post(endpoint, function)となるように記載する
router.post('/welcome_post', welcome_post)

```
### routerで指定した処理を記載する
```python: test_controller.py
from common.beaker import render_template # beakerでrender_templateが使えます

def welcome():
  # flaskのrender_templateと同様の方法をbeakerから使用できる(bakerのrender_templateを使用すればデバッグログで実行内容が出力されます)
  return render_template('welcome.html')
```

## POSTの場合はCSRFトークンが必要になる設定
[CSRF](https://www.ipa.go.jp/security/vuln/vuln_contents/csrf.html)の攻撃に対応するため、POSTリクエストにはトークンを発行して守る機能を特に設定を意識することなく使用できます。(裏を返すとPOSTのForm内になければエラーになる)
この設定しないことあるのか？と常々思っておりならば最初から使えるようにしてしまえ！と思ったことがこの機能を付けたきっかけです！
そのため、POSTを使用する場合は、下記のようにFormにCSRFトークンを埋め込むことが必須になります。
```html
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
```

## ログ出力機能
なんだかんだログを出力する機能はないと困るなと思うことが多く、毎回ログの設定をしているのですが都度設定するのもめんどくさいし、いい感じにならないかなと思っているのでデフォでできるようにしてあります。
基本的にはconfig.ymlに記載すればよいようにしており設定できる内容も絞ってあります。もはや出力場所とログレベルだけ設定するにした方が初心者が分かりやすいと思いますし、上級者はソースコード直で書き換えてね？っていうスタンスです！

* configファイルの設定

```yml
log:
  level: 'DEBUG'
  file_name: './logs/app.log'
```

* ソース内での設定

```python
from common.beaker import logger

def log_test():
  logger.debug('debugログの出力')
  logger.info('infoログの出力')
  logger.warning('warningログの出力')
  logger.error('errorログの出力')

```

## セッションの使用
WEB系の画面を作っているとセッションを使用したいことは多々あると思っており、毎度設定しなければいけないのはめんどくさいなと考えこちらも簡単に使用できるようにメソッドを使えば簡単に利用できるようにしてあります。タイムアウトの設定はconfigから行えるようにしています。また、セッションについてはリクエストがあれば延命するようにしています。
いらない場合は[この個所](https://github.com/KiharaTakahiro/beaker/blob/main/beaker/beaker/common/beaker.py#L278)をコメントアウトすればよいです。
具体的な使い方は下記のような感じです！消す処理ないんかいとは自分でも思っていますが…そのうち…

* configの設定

```yml
app:
  sessionTimeoutMinutes: 30
```

* 実行箇所での設定 

```python
from common.beaker import get_session, set_session

def session_test():
  # セッションの設定
  set_session('key', 'value')
  
  # セッションの取得
  session_value = get_session('key')

```

## DBとの接続およびトランザクション機能
DBとの疎通のないWEBの機能というのもあまりなくこちらもconfigにて設定すると簡単に使用できるようにしてあります。
私がpostgresをよく使用する関係からpostgresに合わせて記載されているのですが[db.py](https://github.com/KiharaTakahiro/beaker/blob/main/beaker/beaker/common/db.py)の中にDBとの疎通部分やSQLの実行に関する処理は集約してあるので、このファイルを良しなに書き換えれば他のDBMSを使用する場合でも簡単に修正を行うことができます。

* configの設定方法

```yml
database:
  host: "localhost"
  dbname: "postgres"
  user: "postgres"
  password: "postgresk"
```

SQLを実際に使用する個所では以下のように実行します！
with句の中で実行するSQLが1トランザクションとなるように書いてあるのでトランザクションを分けたい場合はwith句を分けてもらえれば別のトランザクションとして実行することが可能となります。

### 取得系のSQL(SELECT文の場合)
```python
from common.beaker import start_transaction

def execute_sql():
  with start_transaction() as tx:
    # 複数行取得の場合
    users = tx.find_all({実行するSQL})
    # 最初の一件目取得の場合
    user = tx.find_one({実行するSQL})
```

### 更新系のSQL(INSERT, UPDATE文の場合)
```python
from common.beaker import start_transaction

def execute_sql():
  with start_transaction(read_only = False) as tx:
    tx.save({実行するSQL})

```

### 削除系のSQL(DELETE文の場合)
```python
from common.beaker import start_transaction

def execute_sql():
  with start_transaction(read_only = False) as tx:
    tx.delete({実行するSQL})

```

削除系とか更新系とかは将来を見据えてメソッドを分けました。
テストをするときであったりですとかORMに移行したいと思ったときにここを分けておかないとつらくなるだろうなと思って分けている意図があります。一方で実行するSQLが実際に削除するSQLか？といったチェックは行ってはいないので…そこは要注意です…

## CSV出力機能
CSVを出力するといった機能は割と必要になることも多いので簡単に使用できるようにできないかと考えてこの機能も付けてみました！
書き方は、こんな感じで記載します。
header行の情報を与えてCSVの情報を作成します。
行ごとにCSVヘッダと同じKeyに値を詰めてwrite_rowメソッドを呼び出すことで行情報を追加します。
最後に作成したデータをgetCalueで読み込んでその情報をmake_csv_responseメソッドに渡します。
第二引数がファイル名になっています。

```python
def welcome_get_csv():
  # ヘッダ行
  headers = {
    'test1': 'テスト1', 
    'test2': 'テスト2'
    }

  with create_csv(headers) as cc:
    # 1行目の書き込み
    row1 = {
      'test1': 'テスト1-1',
      'test2': 'テスト2-1'
    }
    cc.write_row(row1)

    # 2行目の書き込み
    row2 = {
      'test1': 'テスト1-2',
      'test2': 'テスト2-2'
    }
    cc.write_row(row2)
    csv_data = cc.getvalue()
  # test.csvとしてダウンロードさせる
  return make_csv_response(csv_data, 'test')
```

## カスタムフィルタの使用
カスタムフィルタは普通に使えると便利な機能である一方で設定もめんどくさいといったことが多いです！！
あらかじめどこに書くかを決めておいてその機能が簡単に使用できるとしたらよいなと思い
[template_filters.py](https://github.com/KiharaTakahiro/beaker/blob/main/beaker/beaker/template_filters.py)に書いたものはカスタムフィルタとして使用可能なようにしてあります。
事前に金額の3桁区切りですとか、リストからのデリミタを使用した結合などの処理を簡単にフィルタとして使用できるように提供してあります。ここをうまく活用してviewの中で内容を整形できるようにできればと考えています。
[template_filters.py](https://github.com/KiharaTakahiro/beaker/blob/main/beaker/beaker/template_filters.py)に記載したメソッドはjinjaテンプレートの中でフィルタとして使用することができます。
具体的にはこんな感じになります。

* template_filters.pyの記載

```python
def convert_money(number, is_none_text = '0'):
  """3桁カンマ区切りの金額に変換する処理
     例) 10000 → 10,000
     注意点) 小数点は未対応のため必要な場合は別のメソッドを作成して対応する
  Args:
      number (number): カンマ区切りにする数値 
      is_none_text (str, optional): numberがNoneの場合の置き換え文字列. Defaults to '0'.
  Returns:
      str: 3桁カンマ区切りの金額を返却
  """
  # 対象がNoneの場合は置き換えようの文字列を返却
  if number is None:
    return is_none_text

  try:
    logger.debug(f'3桁カンマ区切り処理のnumber: {number}')

    # 3桁カンマ区切りの文字列を返却
    return '{:,}'.format(int(number))

  except Exception:
    # HACK: 全体的に表示に関わる部分でエラーにしないようにしたほうが良いと考えて元文字列の返却を行っている
    #       運用の中でエラーの発見を起こすリスクも考えた上で再度検討しても良いと考えている
    logger.warning(f'3桁カンマ区切り処理に失敗しています number: {number}')
    return number
```

* viewでの記載

```html
{{ 1000 | convert_money }}
```
このように書くと1000という文字が3桁のカンマ区切りとなって画面に表示されます！

# 終わりに
いろいろ考えて作っているものの私自身そこまでPythonやFlaskの使用経験があるわけではないので、あまり良い作りになっていないかもしれません。あまり過度に信用して使用するのはやめた方が良いとおもうもののこの部分だけは使おうとか少しでも利用してくれる人がいれば本当にうれしいなと思います。
また、ログイン機能であったりいろいろ追加していけたら面白いなと考えているので今後もゆっくり修正していければなと考えています。