acokikoy's notes

最近気になる=[NoCode, Shopify], I am..=[Python, ウクレレ, マニュアル車, CMS] LoveなWebディレクター

RESTful Web APIを扱う時の基本手法のまとめ(Python利用)

PythonREST APIを扱う時の基本手法まとめ、Python編。

認証を通したり、CRUD操作、JSONファイルの加工などなど、Web APIのハンドリングに汎用で使える操作をまとめる。 oAuth 認証が未完成だけど、ひとまず公開して後から追記予定。

前回Redmine APIをハンドリングするのに、Redmine専用のライブラリPython-Redmine を使って行った。
今回は、同じ RedmineAPIを素材に、
汎用的な Web APIのハンドリング操作を試して、APIを扱う時に毎度調べ直さないでいいように整理した。

利用ライブラリ

準備: ライブラリのimport と、定数(環境固有値)のセット

# 準備:ライブラリのimport と 定数読み込み
import configparser
import json
import re

import requests


# configファイルを読み込んで定数値をセット
# config.ini はこんな内容だとする
#     [Redmine]
#     url     = https://redmine.ark-web.jp/
#     api_key = 201da518412f8a3fa92086b882d6a81b664d620b

config = configparser.ConfigParser()
config.read('config.ini')
 
REDMINE_URL     = config['Redmine']['url']
REDMINE_API_KEY = config['Redmine']['api_key']
...

認証いろいろ

BASIC認証: authパラメーター で渡す方法

# by curlコマンド
# $ curl -u USERNAME:PASSWORD https://path/to/issues.json?limit=1

# 同じことをpythonで
url = 'https://path/to/issues.json?limit=1'

# BASIC認証方式で認証を通す:
r = requests.get(url, auth=(REDMINE_ID, REDMINE_PW))

if r.status_code == 200:  print('Redmineに接続成功(^o^)')
else:                                 print('Redmineの接続失敗(><)')

BASIC認証: HTTPヘッダで渡す方法 (要: BASE64エンコーディング

Base64エンコーディングの処理

'id:password'をbytes型(utf-8)に変換してBase64エンコードする。
base64ライブラリ

import base64

str = 'id:password'
encoded = base64.b64encode(str.encode('utf-8'))  

print(encoded)      # b'aWQ6cGFzc3dvcmQ='

# 文字列を直書きするならば b'ほにゃらら' のようにバイト型で書く
encoded = base64.b64encode(b'id:password') 

HTTPヘッダ Authorization: Basic aWQ6cGFzc3dvcmQ=BASIC認証

# curlコマンドで
# 生で書くならこう
#     $ curl -H "Authorization: Basic aWQ6cGFzc3dvcmQ=" https://path/to/issues.json?limit=1
# 同時にBASE64エンコーディングもしたいならこう。なお、-nオプションは"改行しない"モード
#     $ curl \
#            -H "Authorization:Basic $(echo -n id:password | openssl base64)" \
#            https://path/to/issues.json?limit=1

# 同じことをpythonで
str = 'id:password'

api_head = {
    'Authorization': 'Basic ' + base64.b64encode(str.encode('utf-8')).decode(),
}

r = requests.get(url, headers=api_head)

APIキー: HTTPヘッダで渡す

# curlコマンドで
#     $ curl \
#            -H 'X-Redmine-API-Key:REDMINE_API_KEY' \
#            https://path/to/issues.json?limit=1

# 同じことをPythonで
api_head = {
    'X-Redmine-API-Key':REDMINE_API_KEY
}

r = requests.get(url, headers=api_head)

BASIC認証配下にあるRedmine環境に、APIキー方式で接続

勤務先の開発環境のように、RedmineBASIC認証エリアにある場合はこうする。

# curlコマンドで
#     $ curl \
#            -H 'X-Redmine-API-Key: REDMINE_API_KEY' \
#            -H "Authorization: Basic $(echo -n BASIC_ID:BASIC_PW | openssl base64)" \
#            https://path/to/issues.json?limit=1

# 同じことをPythonで
str = 'BASIC_ID:BASIC_PW'

api_head = {
    'Authorization': 'Basic ' + base64.b64encode(str.encode('utf-8')).decode(),
    'X-Redmine-API-Key':REDMINE_API_KEY
}

r = requests.get(url, headers=api_head)

【後で書く】認証リクエストで得たアクセストークンを使う方法

  • Movable Type Data API の認証がこの方式
  • 認証リクエストを投げて応答としてセッションIDとアクセストークンを受け取る。
  • 認証が必要なリクエストを行う場合は、アクセストークンを付加したリクエストを行なう。

【後で書く】oAuth 2.0

CRUD操作

GETメソッド - Read系操作

基本

import requests

url =  'https://path/to/issues.json?project_id=360&limit=5'
r = requests.get(url, auth=(REDMINE_ID, REDMINE_PW))

print(f"status: {r.status_code}, response ticket_ids: {[r.json()['issues'][i]['id'] for i in range(len(r.json()['issues']))]}")
# status: 200, response ticket_ids: [51325, 51250, 44738, 44291, 44290]
str = BASIC_ID+':'+BASIC_PW
api_head = {
    'Authorization': 'Basic ' + base64.b64encode(str.encode('utf-8')).decode(),
    'X-Redmine-API-Key':REDMINE_API_KEY,
}
r = requests.put(url, headers=api_head)

URLのクエリ文字列がたくさんある場合: paramsキーワード引数でdictを渡す

url =  'https://path/to/issues.json'
payload = {
        'project_id': 360, 
        'limit': 5 
}

r = requests.get(url, auth=(REDMINE_ID, REDMINE_PW), params=payload)

GETメソッドで、リクエストボディを渡すケース

APIによってはこういうケースがあるらしいのだが、
RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content に、
GETでリクエストボディに なにか書いて渡してもAPIによってはリクエスト自体をrejectするケースがあるし、セマンティックス未定義、と書かれている。
要は推奨された使い方ではないようなので検討対象外。

PUTメソッド - Update系操作

例)指定チケット更新

str = BASIC_ID+':'+BASIC_PW
api_head = {
    'Authorization': 'Basic ' + base64.b64encode(str.encode('utf-8')).decode(),
    'X-Redmine-API-Key':REDMINE_API_KEY,
    'Content-Type': 'application/json'
}

# PUT: dataキーワード引数で渡す(dict型)
payload = {
    "issue": {
        "subject": "Redmine REST API テスト",
        "notes": "Add my 8th note via. python."
    }
}

url = 'https://path/to/issues/51250.json'
r = requests.put(url, headers=api_head, data=json.dumps(payload))

print(f"status: {r.status_code}\n{r.headers}\n\n{r.content}")
  • data=payload(:dict型データ) ・・・エンコードしたデータを送信したい時。Requestsライブラリが自動でエンコードする。
  • data=json.dumps(payload) ・・・エンコードされていないデータを送信したい時。エンコードdict の代わりに string を渡すと、Requestsライブラリはデータをそのまま送信する。

POSTメソッド - Create系操作, 認証でも使うことがある

dataパラメータでデータを渡す

例)チケット新規追加

# POST: dataパラメータで渡す
str = BASIC_ID+':'+BASIC_PW
api_head = {
    'Authorization': 'Basic ' + base64.b64encode(str.encode('utf-8')).decode(),
    'X-Redmine-API-Key':REDMINE_API_KEY,
    'Content-Type': 'application/json'
}

payload = {
    "issue": {
        "project_id": 360,
        "status_id": 4,
        "subject": "Redmine REST API 投稿テスト(POST)",
        "description": "本文ですよ"
    }
}

url = url = 'https://path/to/issues.json'
r = requests.post(url, headers=api_head, data=json.dumps(payload))

#  Create成功時ステータスコードは 201 
if r.status_code == 201:
    print("チケット作成成功")
    print(f"status: {r.status_code}\n{r.headers}\n\n{r.content}")
else:
    print("チケット作成失敗")

DELETE - Delete系操作

url = 'https://path/to/issues/51250.json'
r = requests.delete(url, headers=api_head))

よく使われるステータスコード

  • 200番台: 成功(╹◡╹)♡
    • 200 OK
    • 201 Created
      • リソース生成成功(PUT, POSTのレスポンス)
      • POST(リソース新規追加時) 新規リソースのURLがレスポンスのLocationヘッダに入る
  • 300番台: リダイレクト(灬╹ω╹灬)
    • 301 Moved Permanently
      • 移動先URLがレスポンスのLocationヘッダに入る
    • 303 See Other
      • POSTでリソース操作した結果をGETで取得する時など、リクエストに対する処理結果が別のURIで取得できることを示す
  • 400番台: こっち(クライアント)側に問題あり系 (´・ε・̥ˋ๑)
    • 400 Bad Request
      • リクエスト構文やパラメータ誤り
    • 401 Unauthorized
      • 認証不正
      • レスポンスの WWW-Authenticateヘッダで、あるべき認証方式が示される HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="auth required"
    • 404 Not Found
  • 500番台: サーバ側になんらかトラブル系 orz...

JSONデータの処理あれこれ

APIを扱う時に、JSONデータをPOSTしたり、レスポンスから得られたJSONをパースしたりと、JSONデータの加工機会は多い。 API周りでよく使うJSON処理をまとめる。

JSONの要素にアクセス - データ構造がわかっている場合

r.json()['issues'][0]['project']['name']
# 'XXXXプロジェクト`

JSONデータをきれいに整形表示

import json

data_python = r.json()
print(json.dumps(data_python, indent=4, ensure_ascii=False))
#結果
#    {
#        "issues": [
#            {
#                "id": 51337,
#                 "project": {
#                     "id": 356,
#                     "name": "XXXXXプロジェクト"
#                 },
#               "tracker": {
#                     "id": 6,
#                     "name": "ストーリー"
#                },
#              "status": {
#    ....
  • ensure_ascii パラメータ はデフォルトTrue。非 ASCII 文字はエスケープされる。
  • それだと日本語が読みづらくなるので可読(表示に使う時)にしたければ False にする。

json.dumps()は何をしてくれるモノなのか? - dict のデータをJSON形式のstr型に変換

Sample code of json.dumps() method.

  • json.dumps()は、dictをJSON型のstrデータに変換する。
  • Boolean値 True/False を true/false に、Noneを null に変換
  • "ダブルクォーテーション" で囲む
  • エスケープ文字の処理
  • 日本語(非ascii文字)は ensure_asciiパラメータ=False (default TrueでUnicode化) で可読になる

json.loads()は何をしてくれるモノなのか? - JSON形式のstrデータを dictに変換

Sample code of json.loads() method.

  • json.loads()は、JSON型のstrデータをdict型に変換する。
  • Boolean値 true/false を True/False に、null を None に変換
  • エスケープ文字の処理
  • 日本語(非ascii文字)は Unicodeでも そうでなくても日本語で表示される

json.load() - 外部ファイル filename.json を読み込んで 扱いやすいdict型 に変換

Sample code of json.load() method.

  • json.loads()と間違えやすいが、sなしの方はJSON形式のファイルを dict型として読み込んでくれるもの。
  • 普通の read()で読むのと違って、行末の '\n' を取ったり、strからdictに変換したりいろいろ賢い。

json.dump() - dictデータをJSON形式の文字列に変換して外部ファイル filename.json に書き出し

  • json.dumps()と間違えやすいが、dict を JSON型のstrデータに変換してファイル書き出ししてくれるもの。
  • 日本語(非ascii文字)は ensure_asciiパラメータ=False (default TrueでUnicode化) で 可読になる

Sample code of json.dump() method.