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がアーカイブされるタイミングで同期されるため、遅延が発生する
マスタサーバ設定
- 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
- マスタサーバで強制敵にWALアーカイブを実行
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によるバックアップ・リストア方法
SQLによるバックアップとは
環境
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_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. 開始時間設定
2. ストップウォッチ開始、停止
3. リセット
Flutter習得
習得にはKBOYさんが運営しているFlutter大学の動画を見させていただきました。
環境構築から、簡単なYoutube風アプリ(側だけ)の作成まで、動画を見ながら2日で実施出来ました。
※2020年の動画で少し古いですが動作しました。
- 環境構築 : https://www.youtube.com/playlist?list=PLuLRJz1UnJzE4-HlkLTG8ARbZ2TDBNHzZ
- Flutter基礎 : https://www.youtube.com/playlist?list=PLuLRJz1UnJzEDjRr1XkqyOzFzUi3Df4B0
ストップウォッチ作成
1日でサクッと作成しました。
FlutterPicker というプラグインを使用すると、時間設定が楽に作成できます。
タイマー部分は、Timerで1秒の定期実行でカウントアップの関数を呼び出しています。
画面の更新は、SetStateではなくProviderパッケージを使い、notifyListeners()
で変更を通知しています。
Widgetのツリー
ソースコード
/* 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(); } }
参考
- Flutter大学 : Flutter大学 - YouTube
- FlutterPicker を使う : https://rinoguchi.net/2021/05/flutter-hour-minute-second-input.html
React公式チュートリアル 三目並べ 応用課題
Reactの公式チュートリアルにある三目並べゲーム (tic-tac-toe)を作ってみました。 ゲーム自体は記事の通りに進めれば完成できるので、最後の応用にあった6つの課題を追加実装しています。
応用課題
- 履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。
- 着手履歴のリスト中で現在選択されているアイテムを太字にする。
- Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。
- 着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。
- どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。
- どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。
出来たもの
全ての応用課題を実装したものになります。
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についてそろそろ知っときたいなと思い調べたのでメモ
実際の動作に関係するものではないので、後回ししてました、、
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
設定されている内容
自動生成でチェックの必要がないファイルなどは、事前に設定していてもよいかもしれません。
ユーザごとの設定
構成ファイルを保存しておけば自動的に読み込まれるようです。
OS保存先はOSごとに設定ファイルの保存場所
とファイル名
が違っています。
Windows : ~\.flake8 Linux : ~/.config/flake8
チーム内で共有
各々が作った構成ファイルを使用すると、コードが統一されず読みづらいです。 なので、事前に構成ファイルをgitプロジェクト内に含め共有し、それを使ってflake8を実行すると良いと思います。
# --config オプションを使って、構成ファイルを指定 $ flake8 --config flake8 main.go
最後に
導入が意外と簡単に出来るので、面倒ですがプロジェクトと開始時にやっておくとコードが統一されて良さそう。
SwaggerEditorでAIPを設計してみる
SwaggerEditorを使ってAPIの設計を行ったのでメモ
この記事で行っていること
- Swaggerとは
- OpenAPIとは
- Swager Editorを使ってみる
- Swagger Codegenでコード生成
- FlaskAPIServer 起動
- SwaggerUIでリクエストお試し
- SwaggerEditorを使ってみて
Swaggerとは
API開発をする際に使えるツールが詰まったもの。 以下のツールがオープンソースで誰でも使える
ツール | |
---|---|
Swagger Codegen | OpenAPIの定義からサーバ、クライアントのコードを生成する |
Swagger Editor | OpenAPIの仕様で、APIを設計するエディタ |
Swagger UI | 設計したAPIを表示するビュワー。実際にリクエストを送り、応答を確認できる。 |
OpenAPIとは
- OpenAPI Initiative(OAI)というコミュニティが定めた、API仕様を記述する標準フォーマット
- JSON、YAMLの二つの形式で表すことが可能
- ファイル名デフォルトは
openapi.json
かopenapi.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] を選択
生成されたファイル
選択していないが、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でSwaggerEditor
とFlaskAPIServer
を同時に起動。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!
とレスポンスが返ってくることを確認
SwaggerEditorを使ってみて
- API設計とドキュメント作成(Swagger UI)が同時に出来て楽
- SwaggerEditorでは、SwaggerUIがホットリロードしてくれるので書き易い
- OASで定義できる項目が多いので、前提としてAPI設計について知っておく必要がある
- 生成されたコードから編集するのみでAPIサーバを構築できるので楽
今回は以上です。