Pythonで REST APIを扱う時の基本手法まとめ、Python編。
認証を通したり、CRUD操作、JSONファイルの加工などなど、Web APIのハンドリングに汎用で使える操作をまとめる。 oAuth 認証が未完成だけど、ひとまず公開して後から追記予定。
前回はRedmine APIをハンドリングするのに、Redmine専用のライブラリPython-Redmine を使って行った。
今回は、同じ Redmine のAPIを素材に、
汎用的な Web APIのハンドリング操作を試して、APIを扱う時に毎度調べ直さないでいいように整理した。
利用ライブラリ
- Requests: 人間のためのHTTP — requests-docs-ja 1.0.4 documentation
- json --- JSON エンコーダおよびデコーダ — Python 3.7.5 ドキュメント
- 公式ドキュメントは素っ気なく歯触りが悪いので^^;; 以下 "参考書"
- PythonでJSONファイル・文字列の読み込み・書き込み | note.nkmk.me
- Pythonでjson dumpsを使いこなそう!(encoding、foramt、datetime) | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
- configparser --- 設定ファイルのパーサー — Python 3.7.5 ドキュメント
- base64 --- Base16, Base32, Base64, Base85 データの符号化 — Python 3.7.5 ドキュメント
準備: ライブラリの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キー方式で接続
勤務先の開発環境のように、RedmineがBASIC認証エリアにある場合はこうする。
# 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番台: リダイレクト(灬╹ω╹灬)
- 400番台: こっち(クライアント)側に問題あり系 (´・ε・̥ˋ๑)
- 400 Bad Request
- リクエスト構文やパラメータ誤り
- 401 Unauthorized
- 認証不正
- レスポンスの WWW-Authenticateヘッダで、あるべき認証方式が示される
HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="auth required"
- 404 Not Found
- 400 Bad Request
- 500番台: サーバ側になんらかトラブル系 orz...
- 500 Internal Server Error
- 503 Servive Unavailable
JSONデータの処理あれこれ
APIを扱う時に、JSONデータをPOSTしたり、レスポンスから得られたJSONをパースしたりと、JSONデータの加工機会は多い。 API周りでよく使うJSON処理をまとめる。
JSONの要素にアクセス - データ構造がわかっている場合
r.json()['issues'][0]['project']['name'] # 'XXXXプロジェクト`
JSONデータをきれいに整形表示
- JSONデータをきれいに整形表示
- 参考記事: 【Python】JSONデータの使い方(jsonモジュール) | Hibiki Programming Notes
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に変換したりいろいろ賢い。