postgresql スタンバイサーバ の非同期レプリケーション

環境

postgresql 12.9
Docker 20.10.10

Dockerを使用し、2つのデータベースサーバを起動
ファイル単位のログシッピング、ストリーミングレプリケーションを分けて実行してみる

マスタサーバ

IPアドレス: 172.10.1.2

スタンバイサーバ

IPアドレス: 172.10.1.3

マスタサーバの起動・接続

# ボリューム作成
$ docker volume create master_db # マスタDB保存先
$ docker volume create standby_db # スタンバイDB保存先
$ docker volume create archive_wal # WALアーカイブ先

# ネットワーク作成
$ docker network create --subnet=172.10.1.0/24 sample_nw

# マスタDB コンテナ起動
$ docker run --net=sample_nw --ip=172.10.1.2 -e POSTGRES_PASSWORD=xxxxxxxx -v archive_wal:/archive_wal -v master_db:/var/lib/postgresql/data -d postgres:12.9-alpine

テストデータの追加

-- usersテーブル作成
create table users(
    id serial not null,
    name character varying
);

-- データ追加
insert into users(name) values ('A'),('B'),('C');

-- データ3件確認
postgres=# select * from users;
 id | name 
----+------
  1 | A
  2 | B
  3 | C

レプリケーションユーザ、認証情報の追加

-- ユーザ作成
postgres=# CREATE ROLE repl_user LOGIN REPLICATION RASSWORD 'xxxxxxxx';

-- 作成確認
postgres=# \du
                                   List of roles
 Role name |                         Attributes                         | Member of
-----------+------------------------------------------------------------+-----------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
 repl_user | Replication                                                | {}
  • pg_hba.conf に認証情報の追加。スタンバイサーバからの接続を許可する
$ vi /var/lib/postgresql/data/pg_hba.conf
# TYPE  DATABASE        USER            ADDRESS                 METHOD
host    replication     repl_user       172.10.1.3/32           md5     # 1行追加

ファイル単位のログシッピング

  • アーカイブされたWALを解読し、マスタサーバと同期を行う
  • アーカイブ先は、スタンバイサーバからアクセス出来るディスクに保存する必要がある
  • WALがアーカイブされるタイミングで同期されるため、遅延が発生する

マスタサーバ設定

## postgresql.conf

wal_level = replica # (デフォルト) 
archive_mode = on # WALアーカイブ有効化
archive_command = 'test ! -f /archive_wal/%f && cp %p /archive_wal/%f'  # アーカイブ時に実行するコマンド

スタンバイサーバを一時起動し設定

  • WALがアーカイブされているディレクトリにアクセスできるよう、archive_walを共有してマウントする
  • データベースを起動する前に、バックアップの取得や設定を行う必要があるため、bashコマンドで起動しターミナルへ接続する
$ docker run --net=sample_nw --ip=172.10.1.3 -e POSTGRES_PASSWORD=xxxxxxxx -v archive_wal:/archive_wal -v standby_db:/var/lib/postgresql/data -it postgres:12.9-alpine bash
  • pg_basebackupコマンドを使用し、マスタサーバよりバックアップ取得
# postgresユーザに切り替え
$ su postgres
# バックアップ取得
$ pg_basebackup -h 172.10.1.2 -p 5432 -U repl_user -D /var/lib/postgresql/data
  • スタンバイモードとして起動するため、standby.signalファイルを作成する
$ touch /var/lib/postgresql/data/standby.signal
  • アーカイブされたWALをコピーするため、restore_commandを設定する
## postgresql.conf

restore_command = 'cp /archive_wal/%f "%p"' # WALをアーカイブ先よりコピーする

スタンバイサーバの本起動

  • 一度コンテナを削除し、コマンドを指定せずに再度起動を行う
# コンテナID確認
$ docker ps
$ docker rm [コンテナID]

# スタンバイサーバ起動
$ docker run --net=sample_nw --ip=172.10.1.3 -e POSTGRES_PASSWORD=password -v archive_wal:/archive_wal -v standby_db:/var/lib/postgresql/data -d postgres:12.9-alpine
  • 最初に書き込んだデータが書き込まれているか確認
postgres=# select * from users;
 id | name 
----+------
  1 | A
  2 | B
  3 | C

新規データを追加しスタンバイサーバで確認

  • マスタサーバ 新規で1件レコード追加
postgres=# insert into users(name) values('D');
  • walがアーカイブされていないため、スタンバイサーバではまだ追加されていない
-- スタンバイサーバ
postgres=# select * from users;
 id | name 
----+------
  1 | A
  2 | B
  3 | C
postgres=# select pg_switch_wal();
 pg_switch_wal 
---------------
 0/A000308
  • スタンバイサーバで新規データが追加されていることを確認
postgres=# select * from users;
 id | name 
----+------
  1 | A
  2 | B
  3 | C
  4 | D  # 追加されている
  • restore_commandが常時実行されているため、WALがアーカイブされていない時は、失敗ログが出力されていた
cp: can't stat '/archive_wal/00000001000000000000000B': No such file or directory
cp: can't stat '/archive_wal/00000002.history': No such file or directory

ストリーミングレプリケーション

  • ログシッピングと違い、WALの変更分をすぐにスタンバイへ適用する。そのため、ログシッピングより最新の状態で同期出来る。

スタンバイサーバを一時起動し設定

  • ログシッピングと同様に、設定のためbashコマンドで起動しターミナルへ接続する
$ docker run --net=sample_nw --ip=172.10.1.3 -e POSTGRES_PASSWORD=password -v standby_db:/var/lib/postgresql/data -it postgres:12.9-alpine bash
  • pg_basebackupコマンドを使用し、マスタサーバよりバックアップ取得
  • --write-recovery-confオプションで、standby.signalファイルの生成や、postgresql.auto.confファイルを生成し、ストリーミングレプリケーションの接続設定を同時に行える
# postgresユーザに切り替え
$ su postgres
# バックアップ取得
$ pg_basebackup -h 172.10.1.2 -p 5432 -U repl_user -D /var/lib/postgresql/data --write-recovery-conf

# postgresql.auto.conf ファイル確認
$ cat /var/lib/postgresql/data/postgresql.auto.conf

# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=repl_user password=xxxxxxxx host=172.10.1.2 port=5432 sslmode=prefer sslcompression=0 gssencmode=prefer 
krbsrvname=postgres target_session_attrs=any'

スタンバイサーバの本起動

  • こちらも同様に、一度コンテナを削除し、コマンドを指定せずに再度起動を行う
# コンテナID確認
$ docker ps
$ docker rm [コンテナID]

# スタンバイサーバ起動
$ docker run --net=sample_nw --ip=172.10.1.3 -e POSTGRES_PASSWORD=password -v archive_wal:/archive_wal -v standby_db:/var/lib/postgresql/data -d postgres:12.9-alpine
  • 最初に書き込んだデータが書き込まれているか確認
postgres=# select * from users;
 id | name 
----+------
  1 | A
  2 | B
  3 | C

新規データを追加しスタンバイサーバで確認

  • マスタサーバ 新規で1件レコード追加
postgres=# insert into users(name) values('D');
  • ログシッピングと違い、walのアーカイブを待たずスタンバイサーバで同期されている事を確認
-- スタンバイサーバ
postgres=# select * from users;
 id | name 
----+------
  1 | A
  2 | B
  3 | C
  4 | D  # 追加されている

ストリーミングレプリケーションの確認

postgres=# select usename,client_addr,sent_lsn,write_lsn,flush_lsn,replay_lsn from pg_stat_replication;
  usename  | client_addr | sent_lsn  | write_lsn | flush_lsn | replay_lsn 
-----------+-------------+-----------+-----------+-----------+------------
 repl_user | 172.10.1.3  | 0/D000318 | 0/D000318 | 0/D000318 | 0/D000318

postgresql SQLによるバックアップ・リストア方法

f:id:wood__stock:20220121154156p:plain

SQLによるバックアップとは

  • バックアップファイルとして、SQLコマンドを生成する
  • 他のDBサーバへそのファイルを実行することで、同じ状態を再構築出来る
  • バージョン違いや、マシンアーキテクチャの違うDBサーバ間でも実行出来る

環境

postgresql 12.4

テストデータの作成

  • sampleデータベース作成
  • usersテーブル作成し、テストデータを3件追加
  • restore_sampleリストア先のデータベース作成
-- db作成
create database sample;

-- usersテーブル作成
create table users(
    id serial not null,
    name character varying
);

-- データ追加
insert into users(name) values ('A'),('B'),('C');

-- リストア先データベース作成
create database restore_sample;

データベース単体に対して実行

  • データベース毎に個別でバックアップを行う

バックアップ

$ pg_dump sample > dump_sample

リストア

  • リストア実行途中にエラーが発生した場合(ユーザが作成されていない等)でも、継続してリストアを続ける
    • 中断したい場合は--set ON_ERROR_STOP=onを設定する。
    • 中断した場合、それまで実行した不完全なデータが残る
  • --single-transactionオプションを使用し1つのトランザクションと実行することで、不完全なデータが残らないようにすることも可能
$ pg_dump restore_sample < dump_sample

# リストア途中でエラーが発生した場合、中断する
$ pg_dump --set ON_ERROR_STOP=on restore_sample < dump_sample

# 1つのトランザクションとして実行し、中断した場合でも不完全なデータを残さない
$ psql --single-transaction restore_sample < dump_sample

データベースクラスタ全体に対して実行

  • pg_dumpと違い、データベースクラスタ全体をバックアップする
  • クラスタレベルのみや、テーブル、ロールなど、分けてバックアップ可能
    • -a, --data-only: データのみ
    • -g, --globals-only: データベース以外の、クラスタレベルの情報のみ
    • -O, --no-owner: リレーションシップの省く
    • -r, --roles-only: ロールのみ
    • -s, --schema-only: スキーマのみ
    • -t, --tablespaces-only: テーブルスペースのみ スキーマ+データ

バックアップ

$ pg_dumpall > dump_all

# クラスタレベルの情報のみバックアップ
$ pg_dumpall --globals-only > dump_all

リストア

# 他サーバのデータベースに対して、リストアを実行
psql -h [ホスト名:IPアドレス] -p 5432 -f dump_all postgres

大規模データベースの場合

  • バックアップ時に出力されるファイルサイズが大きくなるため、圧縮を行う

バックアップ

  • カスタムバックアップ書式-Fを使用することで、テーブルの復元を部分的に行えるらしい(わからん)
$ pg_dump -Fc sample > dump_sample

リストア

  • カスタムバックアップ書式を使用した場合、pg_restoreコマンドを使用する必要がある
$ pg_restore -d restore_sample dump_sample

並列実行

  • 大きなデータベースの場合、バックアップ・リストアを高速で実行できる
  • 並列ジョブ数を-jで設定出来る
  • バックアップは出力フォーマット-F dとし、ディレクトリフォーマットに設定する必要がある
# 並列ジョブ3でバックアップ
pg_dump -j 3 -F d -c -f dump_sample_dir sample
# 並列ジョブ3でリストア
pg_restore -j 3 -d restore_sample dump_sample_dir

Flutter 初めてのアプリ作成 ストップウォッチ

今回はFlutterで簡単なストップウォッチを作成してみました。

環境

  • OS : Windous10
  • Flutter : v2.2.3

完成品

0秒からではなく、どの時間からでも開始できるストップウォッチにしてみました。
ストップウォッチを使っていて、途中から始めたい事がよくあったので、、、
機能は3つです。

1. 開始時間設定

f:id:wood__stock:20210812105432g:plain

2. ストップウォッチ開始、停止

f:id:wood__stock:20210812105456g:plain

3. リセット

f:id:wood__stock:20210812105533g:plain

Flutter習得

習得にはKBOYさんが運営しているFlutter大学の動画を見させていただきました。
環境構築から、簡単なYoutube風アプリ(側だけ)の作成まで、動画を見ながら2日で実施出来ました。
※2020年の動画で少し古いですが動作しました。

ストップウォッチ作成

1日でサクッと作成しました。
FlutterPicker というプラグインを使用すると、時間設定が楽に作成できます。
タイマー部分は、Timerで1秒の定期実行でカウントアップの関数を呼び出しています。
画面の更新は、SetStateではなくProviderパッケージを使い、notifyListeners()で変更を通知しています。

Widgetのツリー

f:id:wood__stock:20210812111532p:plain

ソースコード

/*
main.dart
メイン画面
*/

import 'package:flutter/material.dart';
import 'package:flutter_picker/Picker.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';

import 'main_model.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ストップウォッチ',
      home: ChangeNotifierProvider<MainModel>(
        create: (context) => MainModel(),
        child: Scaffold(
          appBar: AppBar(
            title: Text('ストップウォッチ'),
          ),
          body: Consumer<MainModel>(
            builder: (context, model, child) {
              return Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    TextButton( // タイマー時間表示 + Editボタン
                      onPressed: () {
                        if (!model.isEditTimeEnabled) return;
                        // 時間設定ウインド表示
                        Picker(
                          adapter: DateTimePickerAdapter(
                            type: PickerDateTimeType.kHMS,
                            value: model.timerTime,
                            customColumnType: [3, 4, 5],
                          ),
                          title: Text("Select Time"),
                          onConfirm: (Picker, List values) {
                            model.changeTimerTime(DateTime.utc(
                                0, 0, 0, values[0], values[1], values[2]));
                          },
                        ).showModal(context);
                      },
                      child: Text(DateFormat.Hms().format(model.timerTime),
                          style: TextStyle(
                            fontSize: 80,
                          )),
                    ),
                    Row( // START,STOP,RESETボタン
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Padding( // STARTボタン
                          padding: const EdgeInsets.all(8.0),
                          child: ElevatedButton.icon(
                              onPressed: !model.isStartEnabled
                                  ? null
                                  : () {
                                      model.startTimer();
                                    },
                              icon: Icon(Icons.play_arrow),
                              label: Text('START')),
                        ),
                        Padding( // STOPボタン
                          padding: const EdgeInsets.all(8.0),
                          child: ElevatedButton.icon(
                              onPressed: !model.isStopEnabled
                                  ? null
                                  : () {
                                      model.stopTimer();
                                    },
                              icon: Icon(Icons.stop),
                              label: Text('STOP')),
                        ),
                        Padding( // RESETボタン
                          padding: const EdgeInsets.all(8.0),
                          child: ElevatedButton.icon(
                              onPressed: !model.isResetEnabled
                                  ? null
                                  : () {
                                      model.resetTimerTime();
                                    },
                              icon: Icon(Icons.clear),
                              label: Text('RESET')),
                        )
                      ],
                    ),
                  ],
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}
/*
main_model.dart
メイン画面のデータモデル
*/

import 'dart:async';

import 'package:flutter/material.dart';

class MainModel extends ChangeNotifier {
  Timer? timer;
  DateTime timerTime = DateTime(0);
  bool isStartEnabled = true;
  bool isStopEnabled = false;
  bool isResetEnabled = true;
  bool isEditTimeEnabled = true;

  // 開始時間変更
  void changeTimerTime(DateTime time) {
    timerTime = time;
    notifyListeners();
  }

  // 開始時間リセット
  void resetTimerTime() {
    timerTime = new DateTime(0);
    notifyListeners();
  }

  // ストップウォッチ開始
  void startTimer() {
    if (timer == null) {
      timer = Timer.periodic(Duration(seconds: 1), (timer) {
        countUpTime(1);
      });
    }

    isStartEnabled = false;
    isStopEnabled = true;
    isResetEnabled = false;
    isEditTimeEnabled = false;
    notifyListeners();
  }

  // ストップウォッチ停止
  void stopTimer() {
    timer!.cancel();
    timer = null;
    notifyListeners();

    isStartEnabled = true;
    isStopEnabled = false;
    isResetEnabled = true;
    isEditTimeEnabled = true;
    notifyListeners();
  }

  // 1s カウントアップ
  void countUpTime(int second) {
    timerTime = timerTime.add(Duration(seconds: second));
    notifyListeners();
  }
}

参考

React公式チュートリアル 三目並べ 応用課題

Reactの公式チュートリアルにある三目並べゲーム (tic-tac-toe)を作ってみました。 ゲーム自体は記事の通りに進めれば完成できるので、最後の応用にあった6つの課題を追加実装しています。

React公式チュートリアル

応用課題

  1. 履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。
  2. 着手履歴のリスト中で現在選択されているアイテムを太字にする。
  3. Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。
  4. 着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。
  5. どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。
  6. どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

出来たもの

全ての応用課題を実装したものになります。 f:id:wood__stock:20210810131029g:plain

ソースコードGitHubに上げました。 React自体初めてなので、正しい実装なのか不安ですが参考になれば幸いです。

GitHub - WoodStockn/react-tutorial

pythonでLoggingを使ってみる

実行環境

  • OS : Windows10
  • Python : v3.9.6

Loggingについて

Pythonを入れると標準で入ってくるモジュールです。
logging --- Python 用ロギング機能 — Python 3.9.4 ドキュメント

import logging

セットアップ

loggingインスタンスの生成

直接インスタンス化するのではなく、getLogger(name)を介してインスタンス化する事が推奨されています。

logger = logging.getLogger("hogehoge")
logger = logging.getLogger(__name__)

インスタンスの親子

インスタンスから子を作成。

# 親インスタンス
parentLogger = logging.getLogger(__name__)
# 子インスタンス
childLogger = logging.getLogger(__name__).getChild("hogehoge")

ログレベルの設定

ログを出力する閾値を設定する。
閾値を設定することで、それ以上深刻なログのみ出力される。
ロガーインスタンス初期値 : NOTSET
ルートロガー初期値 : WARNING

レベル 数値
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
NOTSET 0
# レベル定数
CRITICAL = 50
FATAL = CRITICAL
ERROR = 40
WARNING = 30
WARN = WARNING
INFO = 20
DEBUG = 10
NOTSET = 0

# Debugレベルに設定
logger.setLevel(logging.DEBUG)
logger.setLevel(10)

# ルートロガー初期値確認
logger = logging.getLogger()
logger.getEffectiveLevel()  # ->  30

ログ出力

レベルごとにメソッドを使い分けて出力する。

# 各レベルで出力
logger.debug("debug")
logger.info("info")
logger.warnig("warning")
logger.error("error")
logger.critical("critical")

# 変数を出力文字内に入れる
name = "hogehoge"
logger.debug("Name:%s", name)  # ->  Name:hogehoge 

NOTSETについて

初期値のNOTSETレベルは、

ロガーがルートロガーであれば処理される、そうでなくてロガーが非ルートロガーの場合には親ロガーに委譲させる という設定で、NOTSET以外の設定を見つけるまで祖先をたどる。
ルートロガーの初期はWARNINGで設定されているため、レベル設定を行わない場合、WARNING以上しか出力されない。 引用元: Logging HOWTO — Python 3.9.4 ドキュメント

# warning以上しか出力されない
logger = logging.getLogger(__name__)
logger.info("info")  -> 
logger.warning("warning")  # -> warning

# ルートロガーのレベル設定
logging.basicConfig(level=logging.DEBUG)

basicConfigでは、ほかにフォーマットや、ハンドラなどで基本的なロギングの設定を行える

Handler

ログの送り先をハンドラとして設定する

StreamHandler

標準出力(stdout)、標準エラー出力(stderror)へ送信する

# ロガーインスタンス作成
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# Handler作成
sh = logging.StreamHandler()
sh.setLevel(logging.INFO)
# loggerにHandler追加
logger.addHandler(ch)

logger.info("hogehoge") # -> hogehoge

FileHandler

ログ出力をファイルに送信する

# ロガーインスタンス作成
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# # Handler作成 出力ファイル名_LOGFILE.log
fh = logging.FileHandler("./LOGFILE.log")
fh.setLevel(logging.INFO)
logger.addHandler(fh)

logger.info("hogehoge")

# ==LOGFILE.log==
hogehoge

Formatter

表示するメッセージの書式を設定する
フォーマット文字列内に特別な文字列を配置することで、日付やログレベルなどをメッセージに含められる。 フォーマッタの初期値は%(message)sのみで、メッセージだけが出力される

出力される内容
%(asctime)s 日時
%(name)s ロガーインスタンス
%(levelname)s ログレベル
%(message)s メッセージ
# ロガーインスタンス作成
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# Handler作成
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# Formatter作成・追加
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
# loggerにHandler追加
logger.addHandler(ch)

logger.info("hogehoge") # -> 2021-07-18 00:00:00,000 - __main__ - INFO - hogehoge
logger.warning("hogehoge") # -> 2021-07-18 00:00:00,000 - __main__ - WARNING - hogehoge
logger.error("hogehoge") # -> 2021-07-18 00:00:00,000 - __main__ - ERROR - hogehoge
logger.critical("hogehoge") # -> 2021-07-18 00:00:00,000 - __main__ - CRITICAL - hogehoge

設定ファイルよりセットアップ

いままでの設定はコード内で行っていたが、
設定ファイル(.conf)を事前に定義して読み込みを行う事もできる。

# ==logging.conf==

# logger keys "root" 
[loggers]
keys=root 
# handler keys "streamHandler","fileHandler" 
[handlers]
keys=streamHandler,fileHandler
# formatter keys "formatter"
[formatters]
keys=formatter
# "root" logger setup
[logger_root]
level=DEBUG
handlers=streamHandler,fileHandler
# "streamHandler" handler setup
[handler_streamHandler]
class=StreamHandler
level=DEBUG
formatter=formatter
# "fileHandler" handler setup
[handler_fileHandler]
class=FileHandler
level=DEBUG
formatter=formatter
args=('LOGFILE.log', 'w')
# "formatter" formatter setup
[formatter_formatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
# conf読み込み
logging.config.fileConfig("./logging.conf")
# loggerインスタンス生成
logger = logging.getLogger(__name__)

辞書型で定義してセットアップ

コード内で辞書型で定義することもできる。
設定内容は設定ファイルでの設定と同じ。

# 辞書型で設定内容を作成
logging_conf = {
    'version': 1,
    'formatters': {
        'formatter': {
            'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        },
    },
    'handlers': {
        'streamHandler': {
            'level':'DEBUG',
            'class':'logging.StreamHandler',
            'formatter': 'formatter'
        },
        'fileHandler':{
            'level':'DEBUG',
            'class':'logging.FileHandler',
            'formatter': 'formatter',
            'filename': 'LOGFILE.log',
            'mode': 'w'
        },
    },
    'loggers': {
        'root': {
            'handlers': ['streamHandler', 'fileHandler'],
            'level': 'DEBUG',
        }
    }
}

# 辞書型で読み込み
logging.config.dictConfig(logging_conf)
# インスタンス作成
logger = logging.getLogger(__name__)

Pythonで flake8 (Linter,Formatter) を使ってみる

今回は、Linter Formatterについてそろそろ知っときたいなと思い調べたのでメモ
実際の動作に関係するものではないので、後回ししてました、、

f:id:wood__stock:20210711141412p:plain

Lintter, Formatter

Linter ってなに

lintとは、コンピュータプログラムなどのソースコードを読み込んで内容を分析し、問題点を指摘してくれる静的解析ツール。また、そのようなツールで解析を行うこと。ツールを指す場合は “linter” (リンター)と呼ぶこともある。
引用元 : IT用語辞典 e-Words

  • プログラムを実行する前に、問題が発生しそうな箇所を指摘してくれるもの。(変数を宣言してるのに使ってないなど)
  • 解析するプログラムを"リント"、解析ツールを"リンター"と呼ぶ。

Formatter ってなに

定めたルールに従ってコードをチェック・成形するツール。
動作に影響しないコードスタイルについてチームで統一することで、コードを読みやすくするもの。
よくあるルールとしては、空白の入れ方や改行位置などがあるそうです。

flake8

flake8ってなに

Flake8 is a wrapper around these tools: ・ PyFlakes
・ pycodestyle
・ Ned Batchelder’s McCabe script
引用元: flake8 · PyPI

  • PyFlakes : コード規約PEP8に準じ、コードのエラーチェックをするもの。 Lintter
  • pycodestyle : コード規約PEP8に準じ、コードスタイルをチェックするもの。 Formatter
  • Ned Batchelder’s McCabe script : 複雑度チェッカー

個別で存在するのLinter , Formatterをまとめて使えるものまとめて使用できるものだそうです。

インストール

pip install flake8

実行コマンド

$ flake8 [ファイル or ディレクトリ]
$ flake8 ./main.py (ファイル)
$ flake8 ./MyProject (ディレクトリ)

使用例

参考に簡単なコードにflake8を実行してみます。

# == main.py ==

def add(a,b):
    return a+b

if __name__ == "__main__":
    a = 1
    b = 2
    sum = add(a,b)
    print(sum)
# 実行結果

$ python main.py
3
flake8の実行
$ flake8 main.py

main.py:3:10: E231 missing whitespace after ','
main.py:6:1: E305 expected 2 blank lines after class or function definition, found 1
main.py:9:16: E231 missing whitespace after ','
main.py:10:15: W292 no newline at end of file

4つエラーが出ました。
E○○ W○○ とコードが出るのが、チェックしている項目ごとのコードになります。
ErrorCodes

今回出ているエラーとしては以下のようです。

  • E231 : [ , ; : ]の後に空白がありません
  • E305 : 関数またはクラスの終了後に2行の空白行が必要です
  • W292 : ファイルの終わりに改行がありません

特定のエラーのみチェックしたい・省略したい場合はオプションで設定できます。

# 特定のエラーのみチェック
$ flake8 --select E231,E305 main.py

# 特定のエラーを省く
$ flake8--ignore E231,E305 main.pu

構成ファイルの作成

実行時に毎回オプションを設定して、エラー項目を設定するのは大変なので、、、
flake8の構成ファイルを使って読み込めるようにしてみます。

構成ファイルの書き方

flake8コマンドのオプションで設定できる内容を並べていくしていくようです。
参考に、公式ドキュメントで使用されていたものです configuration

# == .flake8 ==

[flake8]
ignore = D203
exclude = .git,__pycache__,docs/source/conf.py,old,build,dist
max-complexity = 10

設定されている内容

  • ignore : チェックしないエラーコード
  • exclude : 除外するファイルまたはディレクト
  • max-complexity : McCabeでチェックする複雑度の閾値

自動生成でチェックの必要がないファイルなどは、事前に設定していてもよいかもしれません。

ユーザごとの設定

構成ファイルを保存しておけば自動的に読み込まれるようです。
OS保存先はOSごとに設定ファイルの保存場所ファイル名が違っています。

Windows : ~\.flake8
Linux : ~/.config/flake8

チーム内で共有

各々が作った構成ファイルを使用すると、コードが統一されず読みづらいです。 なので、事前に構成ファイルをgitプロジェクト内に含め共有し、それを使ってflake8を実行すると良いと思います。

# --config オプションを使って、構成ファイルを指定
$ flake8 --config flake8 main.go

最後に

導入が意外と簡単に出来るので、面倒ですがプロジェクトと開始時にやっておくとコードが統一されて良さそう。

SwaggerEditorでAIPを設計してみる

f:id:wood__stock:20210704213328p:plain

SwaggerEditorを使ってAPIの設計を行ったのでメモ

この記事で行っていること

  • SwaggerEditorを使ってAPI設計
  • SwaggerCodegenでPython-Flaskコードを生成
  • docker-composeで起動、SwaggerUIよりお試しリクエス

Swaggerとは

API開発をする際に使えるツールが詰まったもの。 以下のツールがオープンソースで誰でも使える

ツール
Swagger Codegen OpenAPIの定義からサーバ、クライアントのコードを生成する
Swagger Editor OpenAPIの仕様で、APIを設計するエディタ
Swagger UI 設計したAPIを表示するビュワー。実際にリクエストを送り、応答を確認できる。

OpenAPIとは

  • OpenAPI Initiative(OAI)というコミュニティが定めた、API仕様を記述する標準フォーマット
  • JSONYAMLの二つの形式で表すことが可能
  • ファイル名デフォルトはopenapi.jsonopenapi.yaml

用語の整理

  • OAI = OpenAPI Initiative というコミュニティ、団体
  • OAS = OpenAPI Specification OpenAPIの仕様
  • swagger2.0 = v2.0のOASフォーマット
  • OAS3.0 = v3.0のOASフォーマット

Swager Editorを使ってみる

書き方はこちらを参考に、 簡単なAPIを設計
OpenAPIで定義できる項目が多くて大変💦💦

今回設計するAPI
- BaseURL : http://localhsot:5000/api/v1 - Version : 0.0.1

エンドポイント一覧

メソッド パス
GET /users すべてのユーザ情報を取得
GET /users/{id} 指定idのユーザ情報を取得
POST /users 新規のユーザを登録

openapi.yaml

openapi: 3.0.0
info:
  title: "初めてのSwaggerEditor"
  description: "初めてSwaggerEditorを使って、API設計してみる"
  termsOfService: ""
  version: "0.0.1"
servers:
  - url: "http://localhost:8080/api/v1"
    description: "ユーザ情報を取得するAPIサーバ"
paths:
  /users:
    summary: "Get Users"
    description: "Usersに関する操作"
    get:
      summary: "Get Users"
      description: "全てのユーザを取得します"
      responses:
        200:
          description: "全てのユーザを返す"
          content:
            application/json:
              schema:
                type: string
              examples:
                users:
                  value: [{ "id":1, "name":"Jon", "age":19 },{ "id":2, "name":"Mike", "age":21 }]          
    post:
      summary: "Add User"
      description: "新しいユーザを追加します"
      operationId: "addUser"
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              properties:
                name:
                  description: "ユーザの名前"
                  type: string
                age:
                  description: "ユーザの年齢"
                  type: integer
              required:
                - "name"
                - "age"
      responses:
        201:
          description: "正常にユーザを追加しました"
        400:
          description: "不正なリクエストです"
  /users/{user_id}:
    parameters:
    - name: "user_id"
      in: path
      required: true
      description: "userId"
      schema:
        type: integer
    get:
      summary: "Get One User"
      description: "1人のユーザを取得します"
      responses:
        200:
          description: "1人のユーザを返す"
          content:
            application/json:
              schema:
                type: string
              examples:
                user:
                  value: {"id":1,"name":"Jon","age":19}                  

Swagger Codegenでコード生成

今回は、Python Flask でコードを生成してみる
タブからの[Generate Server]⇒[python-flask] を選択
f:id:wood__stock:20210704165623p:plain

生成されたファイル
選択していないが、Python3.6で作成された
コードはAPIサーバの基本部分のみなので、実際に使用する場合はcontroller.py などに追加する必要がある

swagger_server
│  .dockerignore
│  .gitignore
│  .swagger-codegen-ignore
│  .travis.yml
│  Dockerfile
│  git_push.sh
│  README.md
│  requirements.txt
│  setup.py
│  test-requirements.txt
│  tox.ini
│
├─.swagger-codegen
│      VERSION
│
└─swagger_server
    │  encoder.py
    │  type_util.py
    │  util.py
    │  __init__.py
    │  __main__.py
    │
    ├─controllers
    │      authorization_controller.py
    │      default_controller.py
    │      __init__.py
    │
    ├─models
    │      base_model_.py
    │      users_body.py
    │      __init__.py
    │
    ├─swagger
    │      swagger.yaml
    │
    └─test
            test_default_controller.py
            __init__.py

FlaskAPIServer 起動

Dockerfileも生成されたので、それで起動してみる

SwaggerUIよりリクエストを送信してみたら、CORSエラーが返ってきた
CORSについてか解決できなかったので、docker-composeでSwaggerEditorFlaskAPIServerを同時に起動。Nginxをプロキシサーバとして動かすことで、ドメインを統一してエラー回避

## ファイル構成

{Project}
|- docker-compose.yaml
|- nginx.conf
└─ swagger_server # SwaggerEditor より生成したコード
## docker-compose.yaml

version: "3.8"
services: 
    nginx:
        image: nginx
        ports: 
            - 8080:8080
        volumes:
           - ./nginx.conf:/etc/nginx/conf.d/nginx.conf
    swagger_editor:
        image: swaggerapi/swagger-editor
    flask_api_server:
        build:
            context: ./python-flask-server-generated
            dockerfile: Dockerfile
## nginx.conf

server {
    listen  8080;

    # SwaggerEfitor
    location / {
        proxy_pass http://swagger_editor:8080; 
    }

    # Flask API Server
    location ^~ /api/v1 {
        proxy_pass http://flask_api_server:8080/api/v1; 
    }
}

SwaggerUIでリクエストお試し

起動したAPIサーバに対して、SwaggerUIからリクエストを送ってみると、do some magic!とレスポンスが返ってくることを確認 f:id:wood__stock:20210704210356p:plain

SwaggerEditorを使ってみて

  • API設計とドキュメント作成(Swagger UI)が同時に出来て楽
  • SwaggerEditorでは、SwaggerUIがホットリロードしてくれるので書き易い
  • OASで定義できる項目が多いので、前提としてAPI設計について知っておく必要がある
  • 生成されたコードから編集するのみでAPIサーバを構築できるので楽

今回は以上です。