delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

JSONを返すAPIの共通部分(=エンベロープ)について

概要

JSONを返すAPI実装するときに、以下のように共通部分でラップしたレスポンスを返すことがある。

{
    "header": {
        "code": "0",
        "message": "success"
    },
    "result": {
        "books": [
            {
                "id": 1,
                "name": "技術書1",
                "price": 3000
            },
            {
                "id": 2,
                "name": "技術書2",
                "price": 1500
            }
        ]
    }
}

※ 「共通部分でラップ」とは以下の部分のこと

"header": {
    "code": "0",
    "message": "success"
},
"result": {
    # ここに返したいデータを入れる
}

実装する側になったときに共通部分のことを考えるのが地味に面倒なのと、そもそもこれって要るのかな?って前から気になっていたので調べてみた。

「共通部分」の呼び方について

Web API The Good Parts』によると、この共通部分のことは「エンベロープ(=封筒)」と呼ぶらしい。

エンベロープとは日本語で言うと「封筒」を意味しますが、APIのデータ構造の文脈で言えば、すべてのデータ(レスポンスやリクエスト)を同じ構造でくるむことを言います。
...
メタデータも含んだような形ですべてのAPIが同じデータ構造を前すために実際のデータをくるむための構造をエンベロープと呼びます。

『Web API The Good Parts』 p78〜p79より


たしかに、「API エンベロープ」とか「API envelope」で検索すると情報がたくさん出てくる。

そもそもAPIの共通部分のことを「エンベロープ」って呼ぶっていう知識がないとこの手の議論にアクセスできないんだなと思った。(私は今まで知らなかった。)

エンベロープ」は必要なのか

『Web API The Good Parts』には「エンベロープ」はメタ情報を扱えるので一見便利に見えるが、以下の理由でやるべきではないと記載されていた。

  1. HTTP通信ではHTTPヘッダーが「エンベロープ」の役割を果たす。
  2. 1.より「エンベロープ」は冗長である。

言われてみれば、以下のようにHTTPステータスコードに応じてレスポンスを返せば不要な気がする。

200のとき

{
    "books": [
        {
            "id": 1,
            "name": "技術書1",
            "price": 3000
        },
        {
            "id": 2,
            "name": "技術書2",
            "price": 1500
        }
    ]
}

400のとき

{
    "code": "400-1",
    "message": "invalid parameter xxx."
}

※ 200系の場合と400系や500系の場合でJSONスキーマが異なると、クライアント側でJSONをデシリアライズするときに困るのでは?と思うかもしれないが、クライアント側がそれぞれのステータスコードの場合のJSONスキーマを知っていれば問題ない。

参照系以外のAPIの場合はどうするのか?

次に気になったのは参照系(=CRUDのR以外)の場合はどうするのかということだった。

必要がなければ何も返す必要がないのだが、APIでレスポンスボディに何も返さないというのは、個人的には感覚として気持ち悪い気もする。

調べてみたところ、例えば以下の記事ではC(作成)については記述がなかったが、

  1. U(更新): 更新後のオブジェクトを返す。
  2. D(削除): 204 No Contentを返す。

ことを提案していてなるほどと思った。

medium.com

また、RFCも読んでみた。
https://datatracker.ietf.org/doc/html/rfc2616#section-9.5

RFC2616によると、以下が推奨されていた。

  • POSTで新規作成した場合: 201 Createdを返して作成されたリソースを参照できるもの(恐らく主キーのなど)を返す。
  • PUTで更新した場合: 200204 No Contentを返す。
  • DELETEで削除した場合: 200202 Accepted(削除処理が非同期の場合)か204 No Contentを返す。

自分がAPIを実装するときは、正常なときは固定で200を返してしまうことが多くて、201 Createdとか202 Acceptedとか204 No Contentとか200系のHTTPステータスコードを細かく使い分けたことがなかったので、なるほどと思った。

いったん以上

Pythonでデシジョンテーブルを作成してみた

概要

ソフトウェアテストで使われるデシジョンテーブルという技法がある。

gihyo.jp

前からプログラムで作成できないのかな?と思っていたのだが、やってみたらPythonで意外と少ないコードで実現できた。

以下で実際にデシジョンテーブルを作成しながら説明していく。

【この1冊でよくわかる】ソフトウェアテストの教科書―品質を決定づけるテスト工程の基本と実践』という本のp86の「レンタルDVDの料金割引のデシジョンテーブル」を例とさせて頂く。

使ったライブラリ

  • 標準ライブラリ
    • itertools: 全パターンの組み合わせを出力するのに使う。
  • サードパーティライブラリ
    • pandas: 必須ではないと思うが使うと表の作成とExcel出力が簡単にできる。
    • xlsxwriter: pandasがExcel出力するときに内部で依存している。

やり方

「条件」と「アクション」を定義する。

以下のように「条件」と「アクション」をリストに定義する。

conditions = ["旧作", "年齢65歳以上","年齢18歳以下"]
actions = ["半額", "20%オフ", "10%オフ", "通常料金"]

※ 「条件」、「アクション」はデシジョンテーブルの用語だが、ここでは説明しない。

各条件の「Y/N」の全通りの組み合わせを求める。

以下の3つの条件について、Yes(=Y)の場合とNo(=N)の場合があるので、その全通りの組み合わせを求める。

  1. 旧作
  2. 年齢65歳以上
  3. 年齢18歳以下

※ 2.と3.は両方Yesがありえない条件だが、ここでは考慮しない。

ここで「直積」という考え方を使うとやりたいことができることを知った。

ja.wikipedia.org

すなわち、Y/Nの2つの要素を持つ集合が3つあると考えると、直積を求めればY/Nの全通りの組み合わせを求めることができる。

Pythonではitertools.productという関数を使えば以下のように簡単に書ける。

from itertools import product

combinations = list(product(["Y", "N"], repeat=len(conditions)))

この時点で、combinationsの中身は以下の通り、Y/Nの全通りの組み合わせのリストになっている。

[('Y', 'Y', 'Y'), ('Y', 'Y', 'N'), ('Y', 'N', 'Y'), ('Y', 'N', 'N'), ('N', 'Y', 'Y'), ('N', 'Y', 'N'), ('N', 'N', 'Y'), ('N', 'N', 'N')]

※ 「直積」を知ったきっかけは以下の記事だった。
qiita.com

DataFrameでデシジョンテーブルを作成する。

pandasのDataFrameを使って、プログラム上でデシジョンテーブルを作成する。
※ 普段あまりpandasを使わないので、pandasの使い方はぎこちないかもしれない。

まずはDataFrameを作成してY/Nと条件名を紐付ける。

df = pd.DataFrame(combinations, columns=conditions)

この時点でdfの中身は以下

  旧作 年齢65歳以上 年齢18歳以下
0  Y       Y       Y
1  Y       Y       N
2  Y       N       Y
3  Y       N       N
4  N       Y       Y
5  N       Y       N
6  N       N       Y
7  N       N       N

次にアクション名のカラムを追加する。(値は空文字)

df = df.reindex(columns=conditions + actions, fill_value="")

この時点でdfの中身は以下

  旧作 年齢65歳以上 年齢18歳以下 半額 20%オフ 10%オフ 通常料金
0  Y       Y       Y                    
1  Y       Y       N                    
2  Y       N       Y                    
3  Y       N       N                    
4  N       Y       Y                    
5  N       Y       N                    
6  N       N       Y                    
7  N       N       N                    

最後に、縦と横が逆なので転置する。

df = df.T

これで以下のようにプログラム上でデシジョンテーブルの雛形を作成できた。

         0  1  2  3  4  5  6  7
旧作       Y  Y  Y  Y  N  N  N  N
年齢65歳以上  Y  Y  N  N  Y  Y  N  N
年齢18歳以下  Y  N  Y  N  Y  N  Y  N
半額                             
20%オフ                          
10%オフ                          
通常料金                           

Excelに出力する

「プログラムでのデシジョンテーブルの作り方」という意味ではここまでで終わりなのでおまけだが、最後にExcelに出力する。

ただPandasでExcelに出力するだけなら簡単だが、プログラムの中で列幅も調整すると以下のようにちょっとごちゃごちゃしたコードになった。

writer = pd.ExcelWriter("decision_table.xlsx")
df.to_excel(
    writer,
    sheet_name="cases",
    header=[str(i + 1) for i in df],  # Excel出力時はカラムがNo.1から始まるようにする。
)
# 条件名とアクション名を記載する列のwidthを指定する。
writer.sheets["cases"].set_column(
    0,
    0,
    df.index.map(len).max() * 2  # マルチバイト文字の場合は文字数の2倍のwidthにしたら丁度よかった。
)
for column_idx in df:
    # 各列のwidthを指定する。「Y」か「N」しか入らないことが分かっているので固定長でもいいが、
    # 一応可変長に対応した書き方にした。
    column_width = max(df[column_idx].astype(str).map(len).max(), len(str(column_idx)))
    writer.sheets["cases"].set_column(column_idx + 1, column_idx + 1, column_width)
writer.save()

この辺は以下の記事を参考にさせて頂いた。

towardsdatascience.com

stackoverflow.com

成果物

以下のようなExcelの成果物ができた。

f:id:kamatimaru:20210707005012p:plain

あとは条件に対応する正しいアクションを手で埋めていけばよい。

f:id:kamatimaru:20210707005301p:plain

以上

PythonでHTTPステータスコードを表現するときにhttpモジュールに定義されている定数を使う

Pythonでは標準モジュールのhttpステータスコードが定数で定義されていることを最近知った。
docs.python.org

今までは知らなかったので、ステータスコードで判定する処理を書くときは

if response.status_code == 200:
    print("OK")

みたいにちょっとしたコードならマジックナンバーでべた書きするか

HTTP_STATUS_CODE_BAD_REQUEST = 400

のように自前で定数を定義していた。


httpモジュールに定義されている定数を使うと以下のように書ける。

from http import HTTPStatus

if response.status_code == HTTPStatus.OK:
    print("OK")

今後はこれを使う。

DjangoのUserモデルのlast_loginについて

概要

DjangoのUserモデルにはlast_loginというフィールドがある。

Djangoに限らず、こういう系のカラムをDBに持つことはシステム上よくあるが

  • 最後に「ログイン」という行為をした日付が入る
  • 最後にログイン状態でアクセスした日付が入る

のどちらの仕様なのかというのは、ユーザーからの問い合わせに応じて調査するときや、アクティブユーザー数を調べるときにけっこう重要なので、一応検証してみた。(恐らく前者だろうと思いつつ)

結果

やはり前者だった。

その他

last_loginを保存するときどうやっているのだろう?とソースコードを見ていたら、SIgnalsを使っていて、こういう使い方するんだと思った。

github.com
github.com

RabbitMQのチュートリアルをやってみる(その2)

概要

前回に引き続きRabbitMQのチュートリアルをやってみる。

今回はチュートリアル1の「"Hello World!"」をやる。

www.rabbitmq.com

やったこと

RabbitMQに接続する

まずはRabbitMQに接続する。

チュートリアル上のコードは以下となっているが、RabbitMQにID/PASSWORDを設定している場合は、これだけだと認証エラーになってしまう。

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
connection.close()

エラー

pika.exceptions.ProbableAuthenticationError: ConnectionClosedByBroker: (403) 'ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile.'

以下のように認証情報を渡す必要がある。

import pika

credentials = pika.PlainCredentials("admin", "password")
connection = pika.BlockingConnection(
    pika.ConnectionParameters("localhost", 5672, "/", credentials)
)
channel = connection.channel()
connection.close()

※ 公式ドキュメントは以下
pika.readthedocs.io

キューを作成する

以下で「hello」という名前のキューを作成する。
※ 事前にRabbitMQにキューを作成しておく必要はない。

channel.queue_declare(queue="hello")

以下のように、管理画面上でキューが作成されていることを確認できる。

f:id:kamatimaru:20210627002803p:plain

メッセージを送信する

以下で「Hello World!」というメッセージをキューに送信する。

channel.basic_publish(exchange="", routing_key="hello", body="Hello World!")

以下のように、管理画面上でメッセージを確認できる。

f:id:kamatimaru:20210627004704p:plain

メッセージを受信する

次にメッセージを受信する側のコードを書く。
別のPythonファイルに以下を書いて実行すると、プロセスが実行中の状態になる。

import pika

credentials = pika.PlainCredentials("admin", "password")
connection = pika.BlockingConnection(
    pika.ConnectionParameters("localhost", 5672, "/", credentials)
)
channel = connection.channel()
channel.queue_declare(queue="hello")

def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)

channel.basic_consume(queue="hello", on_message_callback=callback, auto_ack=True)
channel.start_consuming()

この状態で、別のターミナルなどからメッセージを送信すると、メッセージがコンソール上に出力される。

$ python receive.py
 [x] Received b'Hello World!'
 [x] Received b'Hello World!'
 [x] Received b'Hello World!'

また、管理画面上でキュー上のメッセージは空になってることを確認できる。

f:id:kamatimaru:20210627005647p:plain

その他に試してみたこと

チュートリアルにはないが以下を試してみた。

送信側のqueue_declareのキュー名を変える。

送信側のqueue_declareのキュー名を以下のように変えてみた。

channel.queue_declare(queue="hello2")

メッセージ送信先のキューと受信側が待ち受けているキューが違うので、受信側はメッセージを受信できなくのではないかと予想していたが、結果はメッセージを受信した。

ドキュメントをよく読むと、queue_declareがやることは以下の2つとのことだった。

  • キューが存在するかチェックする。
  • キューが存在しなければ作成する。

Declare queue, create if needed. This method creates or checks a queue.

pika.readthedocs.io

従って、「どのキューにメッセージを送信するか」を指定しているのはqueue_declareではなかった。

「どのキューにメッセージを送信するか」を指定しているのは、basic_publishrouting_keyの方だった。

The queue name needs to be specified in the routing_key parameter.

routing_keyのキュー名を変える。

上記を踏まえて、basic_publishrouting_keyのキュー名を以下のように変更してみた。

channel.basic_publish(exchange="", routing_key="hello2", body="Hello World!")

今度は予想通り、受信側はメッセージを受信できなかった。

尚、存在しないキュー名を指定しても特にエラーは発生しなかったが、これは「存在しないキューに送られたメッセージは捨てられる」というRabbitMQの仕様らしい。

If we send a message to non-existing location, RabbitMQ will just drop the message.

stackoverflow.com

以上

【Reactの勉強】アプリをNetlifyにデプロイする

概要

以下の日本大学文理学部情報科学科の教授の方がクリエイティブ・コモンズで公開してくださっているチュートリアルをやっていた。

zenn.dev

前回まででアプリはできたので、Netlifyにデプロイしてみた。

laughing-lamarr-502333.netlify.app

チュートリアルの方には、ローカルでビルドしたファイルを手でアップロードする方法が紹介されていたが、GithubとNetlifyを連携して、GithubにpushしたらNetlifyにデプロイされるようにできたので、そちらの方法でやった。

説明

とても簡単で、10分くらいでできた。

やったことは以下。

  1. Netlifyのアカウントを作成する。
  2. Github側でNetlifyとの連携を認証する。
  3. Github側でデプロイ対象のリポジトリにNetlifyのアプリをインストールする。
  4. ビルドの設定。

2.と3.は流れに沿ってボタンをポチポチ押していけば自然にできる。

4. についても、デプロイ対象のリポジトリとブランチさえ選択すれば

がデフォルトで設定されているので、特に設定値を変える必要はない。

f:id:kamatimaru:20210626212000p:plain

後は、mainブランチにpushすれば自動でデプロイされる。

はまったこと

初回のビルドに失敗したのだが、ビルドログを見たら、以下のWARNINGが出ていた。

src/reportWebVitals.ts
  Line 3:25:  Missing return type on function  @typescript-eslint/explicit-module-boundary-types

ローカルでnpm run buildを実行したところ、やはりWARNINGは出たが、ビルド自体は成功した。

WARNINGレベルでもビルドが失敗するのがNetlifyの設定によるものなのか謎だったが、とりあえず、以下のようにWARNINGを抑制したところ、ビルドに成功した。

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
  // 省略
};

その他

SPAの開発が初めてなので、ローカルでnpm run buildを実行して、ビルド結果はどのようなファイルが生成されるのか見てみた。

$ tree build
build
├── asset-manifest.json
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── static
    ├── css
    │   ├── 2.632c9fdd.chunk.css
    │   ├── 2.632c9fdd.chunk.css.map
    │   ├── main.78575b65.chunk.css
    │   └── main.78575b65.chunk.css.map
    └── js
        ├── 2.2683834e.chunk.js
        ├── 2.2683834e.chunk.js.LICENSE.txt
        ├── 2.2683834e.chunk.js.map
        ├── 3.d06e26fb.chunk.js
        ├── 3.d06e26fb.chunk.js.map
        ├── main.2e327d47.chunk.js
        ├── main.2e327d47.chunk.js.map
        ├── runtime-main.18fa0ab9.js
        └── runtime-main.18fa0ab9.js.map

3 directories, 20 files
$

jsファイルを開いてみると、minifyされたJSが入っていた。
なるほど、こんな感じになるのかと思った。

チュートリアルをやった感想

今回でチュートリアルが一通り終わった。

他のチュートリアルをやった訳ではないので比較はできないが、以下の点でとてもよいチュートリアルだと思った。

  1. 外部のAPIを叩いて表示する処理の実装を経験できる。
  2. デプロイまでの流れを体験できる。

1. については、業務でSPAを実装する場合は、APIを使わないということはほぼないと思うので、そこがチュートリアルに含まれているのはよかったと思った。
(例えば、ブラウザゲームを作ってみる系のチュートリアルだと、APIを使う場面がなかったりする。)

2. については、従来のサーバーサイドのアプリとSPAでは「デプロイ」という概念が大分違うので、デプロイまでやってみるというのは大事だと思った。

最後に

チュートリアルはこれで終わりだが、引き続きReactは勉強したいので、次何をやるか考えたい。

クリエイティブ・コモンズで公開されているコンテンツを使うときの表記方法

概要

クリエイティブ・コモンズで公開されているコンテンツを変更して公開するときに、何を明記すればいいのか分からなかったので調べた。

背景

以下のReactの教材を自分でやったものをGithubとNetlifyで公開しようと思った。
zenn.dev


教材には

この作品はクリエイティブ・コモンズ 表示 4.0 国際 ライセンスの下に提供されています。

と記載されているので、ちゃんとライセンスを明記したほうがよいんだろうなと思った。

調べたこと

種類が6種類ある。

以下を読むと、クリエイティブ・コモンズの中でもライセンスが6種類あるとのことである。

creativecommons.jp

Reactの教材は以下の「表示」という最も自由度の高いCCライセンスだった。

creativecommons.jp

原作者のクレジット(氏名、作品タイトルなど)を表示することを主な条件とし、改変はもちろん、営利目的での二次利用も許可される最も自由度の高いCCライセンス。

表記すること

「表示(BY)」の場合に表記すべきことは以下に記載されている。
※ 「appropriate credit」というリンクを押すとポップアップが表示される。

creativecommons.org

以下引用

If supplied, you must provide the name of the creator and attribution parties, a copyright notice, a license notice, a disclaimer notice, and a link to the material.

それぞれの言葉について調べた。

  • copyright notice: 著作権表示
  • license notice: 恐らく利用元のコンテンツのライセンスのこと
  • disclaimer notice: 免責事項

また、「If supplied」と書いてあるので、利用元のコンテンツに上記が明記されている場合のみ、記載する必要があるのだと解釈した。

以上より、今回のケースであれば、以下を記載すればよさそう。

加えて、以下のQAによると、「合理的であればどのような方式でも行うことができます」とのことなので、特に書き方に決まりもなさそうである。
creativecommons.jp

以上