【Django】複合ユニーク制約を実装する
Djangoで複合主キーっぽいことをやりたくて。
使用技術
* Django 2.2.1 * Python 3.7.3
Djangoでは単一主キーのみをサポートしているため複合主キーはできないのですが 複合ユニーク制約ならできるようです。
Django2.2にて、MetaオプションにUniqueConstraintというものが登場しました。
models.py
from django.db import models class Reservation(models.Model): room = models.CharField(verbose_name="部屋名", max_length=20) date = models.DateField(verbose_name="予約日") class Meta: constraints = [ # 同じ日に部屋の予約を重複させない models.UniqueConstraint(fields=['room', 'date'], name='unique_booking'), ]
※こちらよりコードを引っ張りました。
Django 2.2 LTS 主な変更点まとめ – CreditEngine Tech – Medium
class Metaにて、constraintsに制約を設定するオブジェクトの配列を代入します。
上記では、UniqueConstraintの引数「fields」に設定した'room', 'date'の2つのカラムが複合ユニークとなります。
https://docs.djangoproject.com/ja/2.2/ref/models/constraints/#django.db.models.UniqueConstraint
なお、UniqueConstraintに関してはバリデーションの代わりとはならず、既存組み合わせのレコードを登録しようとするとIntegurityErrorが発生します。
>>> import datetime >>> from .models import Reservation >>> # 正常にcreateが実行される Reservation.objects.create(room='room_a', date=datetime.date(2019, 6, 1)) >>> # 既存のroomとdateの組み合わせをcreateしようとするとIntegurityErrorとなる Reservation.objects.create(room='room_a', date=datetime.date(2019, 6, 1)) # django.db.utils.IntegrityErrorが発生する
なので、バリデーションをしたい場合は
例えば下記のように実装します。
まずは指定されたroomとdateの組み合わせがすでにDBに登録されているか確認するメソッドを作成します。
models.py
import datetime from django.db import models class Reservation(models.Model): room = models.CharField(verbose_name="部屋名", max_length=20) date = models.DateField(verbose_name="予約日") class Meta: constraints = [ # 同じ日に部屋の予約を重複させない models.UniqueConstraint(fields=['room', 'date'], name='unique_booking'), ] # 追加 @classmethod def check_duplicate(cls, room: str, date: datetime.date) -> bool: """同じ日に同じ部屋がすでにDBに登録されているどうかを判定します 登録されていたらTrue, されていなかったらFalseを返します。 """ return cls.objects.filter(room=room, date=date).exists()
続いて、フォームで下記のように実装します。
今回はdjango.formsのFormとModelFormの両方で作成してみました。
どちらを使ってもroomとdateの重複をフォームのバリデーションの時点で確認できることが分かりました。
forms.py
import datetime from django import forms from .models import Reservation class ReservationForm(forms.Form): room = forms.CharField(max_length=20) date = forms.DateField() def clean(self): cleaned_data = super(ReservationForm, self).clean() room = cleaned_data.get("room") date = cleaned_data.get("date") if Reservation.check_duplicate(room=room, date=date): raise forms.ValidationError(f'{date}に{room}はすでに予約されているため登録できません') return cleaned_data class ReservationModelForm(forms.ModelForm): class Meta: model = Reservation fields = ['room', 'date']
python manage.py shellで実行すると下記のようになりました。
>>> # python manage.py shellにて >>> # DBには room='room_a', date=datetime.date(2019, 6, 1)のレコードがすでに存在する状態 >>> from app.forms.reservation_form import ReservationForm, ReservationModelForm >>> # まずはforms.Formで実装したフォーム >>> form_a = ReservationForm({'room':'room_a', 'date':datetime.date(2019,6,1)}) >>> form_a.is_valid() False >>> form_a.errors {'__all__': ['2019-06-01にroom_aはすでに予約されているため登録できません']} >>> # forms.ValidationErrorで設定したエラーメッセージが返される >>> # 続いてforms.ModelFormで実装したフォーム >>> form_b = ReservationModelForm({'room': 'room_a', 'date': datetime.date(2019, 6, 1)}) >>> form_b.is_valid() False >>> form_b.errors {'__all__': ['この 部屋名 と 予約日 を持った Reservation が既に存在します。']} >>> # DjangoのModelFormでデフォルトで実装されているエラーメッセージが返される
また、Metaオプションで複合ユニーク制約する方法として unique_togetherというものもあります。
ただし、こちらに関しては将来廃止される可能性もあると公式ドキュメントにも書いているので Django2.2以降をご利用の場合はUniqueConstraintで実装するようにしましょう。
https://docs.djangoproject.com/en/2.2/ref/models/options/#unique-together
UniqueConstraintのついでに複合インデックスもやってくれてないかなと思って調べたところ、 複合ユニーク制約のみで複合インデックスは別に実装する必要があるようです。
Metaオプションにindexesを追加します。 indexesはmodel.Indexを要素とする配列となります。
Indexの引数fieldsに複合インデックスとしたいフィールドの名前からなる配列を代入してください。
models.py
from django.db import models class Reservation(models.Model): room = models.CharField(verbose_name="部屋名", max_length=20) date = models.DateField(verbose_name="予約日") class Meta: constraints = [ # 同じ日に部屋の予約を重複させない models.UniqueConstraint(fields=['room', 'date'], name='unique_booking'), ] # 複合インデックスの実装 indexes = [ models.Index(fields=['room', 'date']) ]
複合インデックスを設定する方法でMetaクラスにindex_togetherを設定する方法がありますが、 こちらは将来廃止される可能性もあると公式ドキュメントにも書いているので Django2.2以降をご利用の場合はindexesで実装するようにしましょう。
https://docs.djangoproject.com/en/2.2/ref/models/options/#index-together
【Python】GoogleAPIを利用したメソッドのテストでモックを使う
単体テストで何回も外部APIに接続するのはお行儀悪いって聞くけどどう書けば良いのだろう?と思い。
簡単な機能のテストを書きました。
ちょっと変更しましたが、Google Calendar APIのquick startでやっていることをやります。
一つのメソッドが長いので下記のような役割のメソッドに分割しました。
pickleに保存した認証情報をロードする
認証情報が存在しないor有効期限切れならば認証情報を再生成する
イベント情報を取得する役割のオブジェクトを生成する
イベント情報を取得する
今回テストするプロダクトコード↓
event.py
from dataclasses import dataclass from typing import Iterable, Optional, Dict import datetime import pickle import os.path import pathlib import googleapiclient.discovery import google_auth_oauthlib.flow from google.auth.transport.requests import Request import google.oauth2.credentials import iso8601 # 環境変数にした方がよいですが、ブログではわかりやすいように表示しています SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] class ServiceBuilder: @classmethod def renew_token(cls, credentials: google.oauth2.credentials.Credentials) -> google.oauth2.credentials.Credentials: # google.oauth2.credentials.Credentialsは外部APIのクラス if credentials.expired and credentials.refresh_token: # 外部APIに接続 credentials.refresh(Request()) else: # from_client_secrets_file returns google_auth_oauthlib.flow.Flow object flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file( 'credentials.json', SCOPES) credentials = flow.run_local_server() return credentials @classmethod def build(cls, credentials: google.oauth2.credentials.Credentials) -> googleapiclient.discovery.Resource: """トークンを元にServiceを生成します """ # If there are no (valid) credentials available, let the user log in. # TODO: 42-43はrenew_tokenの先頭の方がよい if not credentials.valid: credentials = cls.renew_token(credentials) # 外部APIに接続 return googleapiclient.discovery.build('calendar', 'v3', credentials=credentials) @dataclass class Event: summary: str start: datetime.datetime end: datetime.datetime class EventAdapter: @classmethod def from_google_api(cls, google_event: Dict) -> Event: """GoogleAPIから取得したDict型のイベント情報をEventオブジェクトに変換します Dict型のままだと利用しない情報も含まれている、かつネストが深くて使いづらいため iso8601について https://pypi.org/project/iso8601/ """ return Event( summary=google_event.get('summary'), start=iso8601.parse_date(google_event.get('start').get('dateTime')), end=iso8601.parse_date(google_event.get('start').get('dateTime')) ) class EventManager: @classmethod def fetch_events(cls, number: int, service: googleapiclient.discovery.Resource) -> Iterable[Event]: """Google Calendar APIからイベントを取得します。 number: int 取得するイベントの最大数 service: イベントを取得するResourceオブジェクト """ # service = ServiceBuilder.build() now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time print('Getting the upcoming 10 events') events_result = service.events().list(calendarId='primary', timeMin=now, maxResults=10, singleEvents=True, orderBy='startTime').execute() events = events_result.get('items', []) if not events: return [] return [EventAdapter.from_google_api(event) for event in events]
main.py
import pathlib from typing import Optional import pickle import google.oauth2.credentials import event def load_token(file: pathlib.Path) -> Optional[google.oauth2.credentials.Credentials]: """トークンをロードします。 pickleの中にtokenがなければNoneを返します。 """ try: # pathlib.Path('token.pickle').read_bytes()をモックしたテスト with file.open('rb') as f: return pickle.load(f) except FileNotFoundError: return None def main(): token_file = pathlib.Path('token.pickle') credentials = event.ServiceBuilder.renew_token(credentials=load_token(token_file)) with token_file.open('wb') as f: pickle.dump(credentials, f) service = event.ServiceBuilder.build() # missing one argument events = event.EventManager.fetch_events(number=2, service=service) print(events) if __name__ == '__main__': main()
テストしたいこと
認証情報が有効切れor認証情報がpickleにない場合は新たにトークンが生成されるかどうか
fetch_eventsでイベントが取得されるかどうか。
fetch_eventsでGoogle Calendar APIから取得されたイベントが0件の場合は空の配列が返されるかどうか
from_google_apiとload_tokenに関しては
ちゃんとインスタンスができているか? 例外を発生するか?
といったようにPythonの仕様の確認になってしまうのでテストしないことにしました。
1. 認証情報が有効切れor認証情報がpickleにない場合は新たにトークンが生成されるかどうか
まず、どこが外部APIに接続しているか調べます。
class ServiceBuilder: @classmethod def renew_token(cls, credentials: google.oauth2.credentials.Credentials) -> google.oauth2.credentials.Credentials: # google.oauth2.credentials.Credentialsは外部APIのクラス if credentials.expired and credentials.refresh_token: credentials.refresh(Request()) # 外部APIに接続 else: # from_client_secrets_file returns google_auth_oauthlib.flow.Flow object flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file( 'credentials.json', SCOPES) # 外部APIに接続 credentials = flow.run_local_server() # 外部APIに接続 return credentials
「外部APIに接続」の部分をモックすれば人様のAPIに迷惑をかけない!
renew_token 自体をテストしようとしました。
でも、renew_token自体をテストしても良いけれども、 renew_tokenをまるっとモックしてServiceBuilder.buildで確認しちゃった方が楽だなあと。
credentailsの状態によってbuildの振る舞いが変わるので credentialsのモックを作り、credentialsモックの状態がそれぞれ異なる場合のテストを作ります。
test.py
@unittest.mock.patch('event.ServiceBuilder.renew_token') @unittest.mock.patch('googleapiclient.discovery.build') def test_valid_token(mock_build, mock_renew_token): credentials = unittest.mock.Mock(valid=True) event.ServiceBuilder.build(credentials=credentials) assert not mock_renew_token.called @unittest.mock.patch('event.ServiceBuilder.renew_token') @unittest.mock.patch('googleapiclient.discovery.build') def test_expired_token_has_refresh_token(mock_build, mock_renew_token): credentials = unittest.mock.Mock(valid=False, expired=True, refresh_token='I am refresh token') event.ServiceBuilder.build(credentials=credentials) assert mock_renew_token.called
credentailsの状態が妥当でない場合に認証情報が再生成されるメソッドが呼ばれるかどうかを確認できました!
2. fetch_eventsでイベントが取得されるかどうか。
3. fetch_eventsでGoogle Calendar APIから取得されたイベントが0件の場合は空の配列が返されるかどうか
イベントの取得のメソッドテストでは、イベントをちゃんと取得できるかのみを確認したいです。
serviceにGoogle Calendar APIからイベントを取得させていますが、serviceに利用されているcredentialsが有効期間切れでrefresh_tokenが実行されてしまう・・・
なんてことがあったら面倒です。
なので、fetch_eventsの外でserviceを生成させ、serviceをfetch_eventsの引数にします。
fetch_eventsのテストでは、serviceをモックし、serviceが正常に動くことを前提とすることにしました。
Google Calendar APIのドキュメントを確認すると、 イベント取得メソッドは下記のようになっています。
service.events().list(calendarId='primary', timeMin=now, maxResults=10, singleEvents=True, orderBy='startTime').execute()
https://developers.google.com/calendar/quickstart/python
serviceって何クラスのオブジェクトだと調べました。
from googleapiclient.discovery import build service = build('calendar', 'v3', credentials=creds)
serviceはbuildによって生成されたオブジェクトらしい・・・
googleapiclient.discoveryのbuildはResourceオブジェクトを返すそうです。
https://github.com/googleapis/google-api-python-client/blob/master/googleapiclient/discovery.py#L170
Resourceを探したところ、
googleapiclient.discoveryのResourceだとわかりました。
https://github.com/googleapis/google-api-python-client/blob/master/googleapiclient/discovery.py#L995
ということでserviceはResourceオブジェクトをモックしよう。
service.events().list(calendarId='primary', timeMin=now, maxResults=10, singleEvents=True, orderBy='startTime').execute()
上記をもってGoogle Calendar APIからイベント情報のDictが返ってきます。
ということで、serviceをモックしてこんな感じでイベント情報を返すとします。
実際のAPIではもっとたくさんの情報が返されますが
テストに必要な情報のみにしました。
https://developers.google.com/calendar/v3/reference/events/list
mock_resource.events.return_value.list.return_value.execute.return_value = { "items": [ { "summary": "test", "start": { "dateTime": "2019-06-03T02:00:00+09:00" }, "end": { "dateTime": "2019-06-03T02:45:00+09:00" }, }, { "summary": "test", "start": { "dateTime": "2019-06-03T02:00:00+09:00" }, "end": { "dateTime": "2019-06-03T02:45:00+09:00" }, }, ] }
そして2と3のテストを
@unittest.mock.patch('googleapiclient.discovery.Resource') def test_fetch_events(mock_resource): mock_resource.events.return_value.list.return_value.execute.return_value = { "items": [ { "summary": "test", "start": { "dateTime": "2019-06-03T02:00:00+09:00" }, "end": { "dateTime": "2019-06-03T02:45:00+09:00" }, }, { "summary": "test", "start": { "dateTime": "2019-06-03T02:00:00+09:00" }, "end": { "dateTime": "2019-06-03T02:45:00+09:00" }, }, ] } actual = event.EventManager.fetch_events(number=2, service=mock_resource) assert 2 == len(actual) for item in actual: assert isinstance(item, event.Event) @unittest.mock.patch('googleapiclient.discovery.Resource') def test_fetch_no_events(mock_resource): mock_resource.events.return_value.list.return_value.execute.return_value = {} actual = event.EventManager.fetch_events(number=2, service=mock_resource) assert [] == actual
ポイントは
だと思いました。
外部APIのソースをある程度読むことになり、少し大変でしたが 勉強になりました。
今回のソースコードはこちら
また、NaritoさんがDjangoを利用してGoogle Calendar APIと連携したアプリケーションを作成されていました。
https://torina.top/detail/466/
近いうちにDjangoとGoogle Calendar APIのテストの記事も書こうかなと思います。
(内容はあまり変わらないと思いますが)
【Django】フォームのカスタムバリデーションをテストする
Djangoのフォームで独自のバリデーションを実装した際のテスト方法を紹介します。 間違いがありましたら、ご指摘お願いします(>人<)
今回使ったバージョン
* Python 3.7.1 * Django 2.1.7
今回実装するフォームはこちら
forms.py
import re from django import forms from django.core.exceptions import ValidationError def is_hiragana(value): pattern = r'^[ぁ-ん]+$' if not re.fullmatch(pattern, value): raise ValidationError('ひらがなで書いてください') def validate_chatwork_id(value): pattern = r'^[0-9]+$' if not re.fullmatch(pattern, value) or len(value) != 7: raise ValidationError('半角数字7文字で書いてください') class EmployeeForm(forms.Form): name = forms.CharField( max_length=225, label='名前', widget=forms.TextInput(attrs={'class': 'form-control'}) ) name_kana = forms.CharField( max_length=225, label='名前(かな)', validators=[is_hiragana], required=False, widget=forms.TextInput(attrs={'class': 'form-control'}) ) chatwork_id = forms.CharField( label='チャットワークID', validators=[validate_chatwork_id], widget=forms.TextInput(attrs={'class': 'form-control'}) )
テストしたいこと
名前(かな)にひらがな以外が含まれていたらエラーを表示したい
チャットワークIDが半角数字7文字以外だったらエラーを表示したい
form.is_valid()でTrueかFalseを返すテストは良くみるんだけど、
正しくエラーメッセージが出力されるかの方法がわからん・・・
とりあえず書いてみた。
from django.core.exceptions import ValidationError from django.test import TestCase from .forms import EmployeeForm class TestEmployeeForm(TestCase): @classmethod def setUpClass(cls): pass def test_invalid_form_not_hiragana(self): params = dict( name='サンプル太郎', name_kana='サンプルタロウ', chatwork_id='0123456' ) form = EmployeeForm(params) form.is_valid() self.assertEqual( 'ひらがなで書いてください', form.errors ) @classmethod def tearDownClass(cls): pass
結果、怒られる。
AssertionError: 'ひらがなで書いてください' != {'name_kana': ['ひらがなで書いてください']}
Dict[List]か・・・なら・・・、と試しにやってみる。
def test_invalid_form_not_hiragana(self): params = dict( first_name='サンプル太郎', first_name_kana='サンプルタロウ', chatwork_id='0123456' ) form = EmployeeForm(params) form.is_valid() self.assertTrue( 'ひらがなで書いてください' in form.errors['name_kana'] )
できた!!!
同じ要領でchatwork_idもやってみると
def test_invalid_form_chatwork_id(self): with self.subTest('半角数字以外を含むとエラーになる'): params = dict( first_name='サンプル太郎', first_name_kana='さんぷるたろう', chatwork_id='012aaaaa' ) form = EmployeeForm(params) form.is_valid() self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] ) with self.subTest('7文字以上だとエラーになる'): params = dict( first_name='サンプル太郎', first_name_kana='さんぷるたろう', chatwork_id='012345678' ) form = EmployeeForm(params) form.is_valid() self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] ) with self.subTest('7文字以内だとエラーになる'): params = dict( first_name='サンプル太郎', first_name_kana='さんぷるたろう', chatwork_id='012345' ) form = EmployeeForm(params) form.is_valid() self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] )
form.errorsが何者かみてみると・・・
print(type(form.errors)) # django.forms.utils.ErrorDictが出力される print(type(form.errors['name_kana'])) # django.forms.utils.ErrorListが出力される print(form.errors) # <ul class="errorlist"><li>last_name<ul class="errorlist"><li>この項目は必須です。</li></ul></li><li>name_kana<ul class="errorlist"><li>ひらがなで書いてください</li></ul></li></ul> print(form.errors['name_kana']) # <ul class="errorlist"><li>ひらがなで書いてください</li></ul>
form.errorsはErrorDictというDictを継承したオブジェクトで
form.errorsはErrorListというListを継承したオブジェクトのようです。
ErrorDictのkeyはそのフォームの各フィールドになっているそうです。 ErrorListは各フィールドのエラーの表示方法を決めてるっぽいですね。
print()でulタグが出力されたのは、__str__の処理の中がas_ulだからかと謎が解けました。
https://github.com/django/django/blob/master/django/forms/utils.py
少しテストから話が逸れますが validatorsを使わずclean_field使用してもテストは同じ結果で通りました。
class EmployeeForm(forms.Form): name = forms.CharField( max_length=225, label='名前', widget=forms.TextInput(attrs={'class': 'form-control'}) ) name_kana = forms.CharField( max_length=225, label='名前(かな)', required=False, widget=forms.TextInput(attrs={'class': 'form-control'}) ) chatwork_id = forms.CharField( label='チャットワークID', widget=forms.TextInput(attrs={'class': 'form-control'}) ) def clean_name_kana(self): pattern = r'^[ぁ-ん]+$' name_kana = self.cleaned_data['name_kana'] if not re.fullmatch(pattern, name_kana): raise ValidationError('ひらがなで書いてください') return name_kana def clean_chatwork_id(self): pattern = r'^[0-9]+$' chatwork_id = self.cleaned_data['chatwork_id'] if not re.fullmatch(pattern, chatwork_id) or len(chatwork_id) != 7: raise ValidationError('半角数字7文字で書いてください') return chatwork_id
ちなみに、validatorとclean_fieldの違いについてはこちらの記事をご参照ください。 validatorはそれぞれのフィールドに入力された値をチェックするけれどもフォーム自体の内容は確認しないっぽいですね。
cleanが通らない場合があると考えると、clean_fieldを使った方が無難そう
今回使用したコードまとめ
forms.py
import re from django import forms from django.core.exceptions import ValidationError def is_hiragana(value): pattern = r'^[ぁ-ん]+$' if not re.fullmatch(pattern, value): raise ValidationError('ひらがなで書いてください') def validate_chatwork_id(value): pattern = r'^[0-9]+$' if not re.fullmatch(pattern, value) or len(value) != 7: raise ValidationError('半角数字7文字で書いてください') class EmployeeForm(forms.Form): name = forms.CharField( max_length=225, label='名前', widget=forms.TextInput(attrs={'class': 'form-control'}) ) name_kana = forms.CharField( max_length=225, label='名前(かな)', validators=[is_hiragana], required=False, widget=forms.TextInput(attrs={'class': 'form-control'}) ) chatwork_id = forms.CharField( label='チャットワークID', validators=[validate_chatwork_id], widget=forms.TextInput(attrs={'class': 'form-control'}) ) """こっちでも良い class EmployeeForm(forms.Form): name = forms.CharField( max_length=225, label='名前', widget=forms.TextInput(attrs={'class': 'form-control'}) ) name_kana = forms.CharField( max_length=225, label='名前(かな)', required=False, widget=forms.TextInput(attrs={'class': 'form-control'}) ) chatwork_id = forms.CharField( label='チャットワークID', widget=forms.TextInput(attrs={'class': 'form-control'}) ) def clean_name_kana(self): pattern = r'^[ぁ-ん]+$' name_kana = self.cleaned_data['name_kana'] if not re.fullmatch(pattern, name_kana): raise ValidationError('ひらがなで書いてください') return name_kana def clean_chatwork_id(self): pattern = r'^[0-9]+$' chatwork_id = self.cleaned_data['chatwork_id'] if not re.fullmatch(pattern, chatwork_id) or len(chatwork_id) != 7: raise ValidationError('半角数字7文字で書いてください') return chatwork_id """
test_forms.py(ちょっとテスト増やして整えた)
from django.core.exceptions import ValidationError from django.test import TestCase from .forms import EmployeeForm class TestEmployeeForm(TestCase): @classmethod def setUpClass(cls): pass def test_valid_form(self): params = dict( name='サンプル太郎', name_kana='さんぷるたろう', chatwork_id='0123456' ) form = EmployeeForm(params) self.assertTrue(form.is_valid()) def test_invalid_form_not_hiragana(self): params = dict( first_name='サンプル太郎', first_name_kana='サンプルタロウ', chatwork_id='0123456' ) form = EmployeeForm(params) form.is_valid() with self.subTest('フォームのバリーデーションで妥当ではないと返される'): self.assertFalse(form.is_valid()) with self.subTest('ひらがなで書けというエラーになる'): self.assertTrue( 'ひらがなで書いてください' in form.errors['name_kana'] ) self.assertTrue( 'ひらがなで書いてください' in form.errors['name_kana'] ) def test_invalid_form_chatwork_id_has_charactor(self): params = dict( name='サンプル太郎', name_kana='さんぷるたろう', chatwork_id='012aaaaa' ) form = EmployeeForm(params) form.is_valid() with self.subTest('半角数字以外を含むとフォームのバリーデーションで妥当ではないと返される'): self.assertFalse(form.is_valid()) with self.subTest('半角数字以外を含むと半角数字7文字で書けというエラーになる'): self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] ) def test_invalid_form_chatwork_id_is_over_7(self): params = dict( name='サンプル太郎', name_kana='さんぷるたろう', chatwork_id='012345678' ) form = EmpForm(params) form.is_valid() with self.subTest('7文字以上だとフォームのバリーデーションで妥当ではないと返される'): self.assertFalse(form.is_valid()) with self.subTest('7文字以上だと半角数字7文字で書けというエラーになる'): self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] ) def test_invalid_form_chatwork_id_is_less_than_7(self): params = dict( name='サンプル太郎', name_kana='さんぷるたろう', chatwork_id='012345' ) form = EmpForm(params) form.is_valid() with self.subTest('7文字以内だとフォームのバリーデーションで妥当ではないと返される'): self.assertFalse(form.is_valid()) with self.subTest('7文字以内だと半角数字7文字で書けというエラーになる'): self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] ) @classmethod def tearDownClass(cls): pass
DjangoCongress2019に参加しました
初DjangoConでした!
とても刺激になったので拝聴したトークを自分なりにアウトプットしようと思います。
なお、DjangoCongressJPのHPは以下になります。(TimeTableも載っています)
私が拝聴したのは下記のトークです!
Djangoで静的ファイルとうまくやる話
How to build and deploy a flexible React/Django hybrid application
Djangoでのメール送信 - 設定からテストまで
Authorization in Django
DjangoによるWebエンジニア育成への道
Djangoで静的ファイルとうまくやる話
staticファイルの扱いはなんとなくやっているところがあったので 丁寧に説明してくださってとても為になりました。
Django特有の話だけでなく
「静的」と「動的」の違いやNginxの設定の話もしてくださりました。
Django始めたときに出会いたかった・・・笑
また、AmazonS3を使うと静的ファイルの同期作業から解放されるとのことなので使いたさが増しました!
How to build and deploy a flexible React/Django hybrid application
一部のページではDjangoテンプレートを使い、複雑なUIのページにはReactを使うというお話。
propsをrenderを使って返すという方法は聞いたことなかったので新鮮でした!
今までTemplateしか使っていなかったプロジェクトにReactやVueを導入する際に参考にしたいとも思いました。
Djangoでのメール送信 - 設定からテストまで
Djangoのメール送信機能の仕組みのお話。
基本的な機能の説明はさながら、メール送信のユニットテストの方法やログ出力の方法までご紹介くださりました。
恥ずかしながら、実際にメール送信して挙動確認していたので
ユニットテストでメール文や添付ファイルの確認をする方法は早速仕事で使いたいと思います!
また、運用に備えてログの設定もしたい・・・!
Djangoアプリのデプロイに関するプラクティス
www.slideshare.net
DjangoというよりWeb全般という感じでしたが、構成管理から監視まで幅広くお話してくださり、貴重な資料だなと思いました。 すごすぎて途中から理解できない部分があったので、経験を重ねてからもう一度読み返したいです。
知識と実践を重ねの上に重ねた人なんだな、と思いました。
Authorization in Django
Djangoで複雑な認証を実装するときに役に立つライブラリ「djnago-keeper」の紹介でした。 (グループAの人は○○が出来き、グループBの人は○○と△△が出来る、みたいなやつ)
なんと、発表者であるhirokikyさんご自信が作ったとのこと・・・! すごい・・・
認証対象のユーザーモデルに__acl__メソッドを書き、その中に複雑なロジックを書けば
Viewデコレータをシンプルに保てるというのは便利と思いました。また、変更も__acl__のみで済むのも魅力的でした。
DjangoによるWebエンジニア育成への道
エンジニアリング要素たっぷりというより、とある会社のストーリーという発表でした。
売上至上主義な開発で会社が行き詰まったところから、Pythonに強く付加価値のある開発が出来る会社に成長するまでに歩んだ道という内容です。
個人的に現状行き詰まっているところに重なり響くものが多く、とても勇気をもらいました。
懇親会で発表者のnakazawaさんとお話しましたが、気さくで良い方でした。たくさんアドバイスいただきました。ありがとうございます・・・!
たくさん刺激をもらった一日でした。Djangoについて知らないことを学ぶだけでなく、様々な人とお話してつながりを持てた貴重な機会でした。
初めてあったのに相談に乗ってくださった方もいて、とても感謝しています。
スタッフの皆様、素晴らしい機会を提供してくださりありがとうございました。
来年はトークorLTで登壇できるようにがんばるぞー!
【Django】JsonResponseでのテストの仕方(GET/POST)
久々の投稿となってしまいました(^^;
今回使用したバージョン
* Python 3.7.1 * Django 2.1.7
今回サンプルとしてテストしたいメソッドはこちらのView関数
views.py
import json from django.views.decorators.http import require_POST, require_GET from django.http import JsonResponse from .models import Item @require_GET def get_item(request, id: int) -> JsonResponse: """商品情報を取得するAPIです。 :param request: :param id: ItemモデルのPK :return: JsonResponse """ try: item = Item.objects.get(pk=id) except Item.DoesNotExist: return JsonResponse( data={}, status=404 ) data = { 'name': item.name, 'price': item.price, 'type': item.get_type_display() } return JsonResponse( data=data ) @require_POST def create_item(request) -> JsonResponse: """商品情報を登録するAPIです。 :param request: :return: """ request_data = json.loads(request.body) Item.objects.create( name=request_data.get('name'), price=request_data.get('price'), type=request_data.get('type'), other_type=request_data.get('other_type') ) data = { 'message': '商品を登録しました' } return JsonResponse( data=data )
付随する情報たち
urls.py
from django.urls import path from . import views app_name = 'items' urlpatterns = [ path('<int:id>', views.get_item, name='get'), path('create', views.create_item, name='create'), ]
models.py
from django.db import models from django.core.validators import MinValueValidator class Item(models.Model): """商品モデル """ # 品種 TYPE_CHOICES = ( (0, 'コーヒー'), (1, '紅茶'), (2, 'ココア'), (3, 'タピオカ'), (4, 'その他') ) name = models.CharField(max_length=225) price = models.IntegerField(validators=[MinValueValidator(0)]) type = models.SmallIntegerField(choices=TYPE_CHOICES) other_type = models.CharField(max_length=225, blank=True, null=True) # typeがその他の場合 class Meta: db_table = 'items'
テストしたい事項としては下記
get_item
パラメータで指定したIDの商品情報が返される
存在しないIDがパラメータになったら404エラーとなる
想定しないメソッドでリクエストが送られたら405エラーとなる
create_item
まず、get_itemのテストを。
商品情報が返されるか確認します。
JsonResponseの中身(dataの部分)が正しいか確認したいということで探していると dataの部分はresponse.contentとなっていることがわかりました。
>>> from django.http import JsonResponse >>> response = JsonResponse({'foo': 'bar'}) >>> response.content b'{"foo": "bar"}'
しかし、contentはbytes型なのでそのままではテストできません。
調べたら、json.loads()によってcontentをJSON化して内容をテストしている事例がありました。
結果、get_itemによって返される内容を確認するテストは下記のようになりました。
tests.py
import json from django.test import TestCase, Client from django.urls import reverse from .models import Item class GetItem(TestCase): @classmethod def setUpTestData(cls): cls.item = Item.objects.create( name='ゲイシャ', price=500, type=0 ) cls.client = Client() def test_get_item(self): response = self.client.get( path=reverse('items:get', kwargs={'id': 1}) ) # response.contentをJSON化する content = json.loads(response.content) with self.subTest('商品名が返される'): self.assertEqual( 'ゲイシャ', content['name'] ) with self.subTest('値段が返される'): self.assertEqual( 500, content['price'] ) with self.subTest('品種が返される'): self.assertEqual( 'コーヒー', content['type'] )
また、下記でもよかったようです。
content = response.json()
続きましては、get_itemの異常系のテストです。
意図したステータスコードが返されるか確認します。
django.test.Clientによって作成されたdjango.test.Responseオブジェクトはアトリビュートstatus_codeにステータスコードを保持します。
response.status_codeでそのレスポンスのステータスコードがわかります。
def test_not_existing_item(self): response = self.client.get( path=reverse('items:get', kwargs={'id': 2}) ) content = json.loads(response.content) with self.subTest('コンテントは空である'): self.assertFalse( content ) with self.subTest('404エラーで返される'): self.assertEqual( 404, response.status_code ) def test_request_post(self): response = self.client.post( path=reverse('items:get', kwargs={'id': 2}), data={} ) with self.subTest('405エラーで返される'): self.assertEqual( 405, response.status_code )
続いてcreate_itemのテスト。
下記でいけるかな〜と思いましたが、だめでした(^^;
def test_create_item(self): response = self.client.post( path=reverse('items:create'), data={ 'name': 'ハワイコナ', 'price': 800, 'type': 0 }, )
どうやら、content_type="application/json"がないと正しいcontent_typeでリクエストが送られなかった模様。
デフォルトのcontent_typeがMULTIPART_CONTENTなので、そりゃ何も指定しないでうまくいくはずがありませんでした。
結果、create_itemのテストはこうなりました。
def test_create_item(self): response = self.client.post( path=reverse('items:create'), data={ 'name': 'ハワイコナ', 'price': 800, 'type': 0 }, content_type='application/json' ) content = json.loads(response.content) with self.subTest('商品が登録される'): self.assertTrue( Item.objects.filter(name='ハワイコナ', price=800, type=0).exists() ) with self.subTest('登録完了のメッセージが返される'): self.assertEqual( '商品を登録しました', content['message'] ) def test_request_get(self): response = self.client.get( path=reverse('items:create') ) with self.subTest('405エラーで返される'): self.assertEqual( 405, response.status_code )
2つの関数のテストをまとめるとこうなります。
tests.py
import json from django.test import TestCase, Client from django.urls import reverse from .models import Item class GetItem(TestCase): @classmethod def setUpTestData(cls): cls.item = Item.objects.create( name='ゲイシャ', price=500, type=0 ) cls.client = Client() def test_get_item(self): response = self.client.get( path=reverse('items:get', kwargs={'id': 1}) ) content = json.loads(response.content) with self.subTest('商品名が返される'): self.assertEqual( 'ゲイシャ', content['name'] ) with self.subTest('値段が返される'): self.assertEqual( 500, content['price'] ) with self.subTest('品種が返される'): self.assertEqual( 'コーヒー', content['type'] ) def test_not_existing_item(self): response = self.client.get( path=reverse('items:get', kwargs={'id': 2}) ) content = json.loads(response.content) with self.subTest('コンテントは空である'): self.assertFalse( content ) with self.subTest('404エラーで返される'): self.assertEqual( 404, response.status_code ) def test_request_post(self): response = self.client.post( path=reverse('items:get', kwargs={'id': 2}), data={} ) with self.subTest('405エラーで返される'): self.assertEqual( 405, response.status_code ) @classmethod def tearDownClass(cls): pass class CreateItem(TestCase): @classmethod def setUpTestData(cls): cls.client = Client() def test_create_item(self): response = self.client.post( path=reverse('items:create'), data={ 'name': 'ハワイコナ', 'price': 800, 'type': 0 }, content_type='application/json' ) content = json.loads(response.content) with self.subTest('商品が登録される'): self.assertTrue( Item.objects.filter(name='ハワイコナ', price=800, type=0).exists() ) with self.subTest('登録完了のメッセージが返される'): self.assertEqual( '商品を登録しました', content['message'] ) def test_request_get(self): response = self.client.get( path=reverse('items:create') ) with self.subTest('405エラーで返される'): self.assertEqual( 405, response.status_code ) @classmethod def tearDownClass(cls): pass
まあ、DRFでやれよという話ですが、そちらはおいおい出来たらと思います!
【非公式翻訳】ForeignKey in Django公式ドキュメント
Django公式ドキュメントのForeignKeyの日本語訳です。
間違いがありましたら、コメント欄にてご指摘お願いします!
ForeignKey
class ForeignKey(to, on_delete, **options) [ソース]
多対一のリレーションです。2つの位置引数を必要とします。すなわち、そのモデルに紐付いているクラスとon_deleteオプションです。
再帰的なリレーションを築くために(別の表現をすると、多対一のリレーションをもつオブジェクトをつくるために)、models.ForeignKey('self', on_delete=models.CASCADE)を利用します。
未定義のモデルに紐づくリレーションを築く必要がある場合、モデルオブジェクト自体を使うのではなく、モデルの名前を使うことができます。
from django.db import models class Car(models.Model): manufacturer = models.ForeignKey( 'Manufacturer', on_delete=models.CASCADE, ) # ... class Manufacturer(models.Model): # ... pass
具体的なモデルとしてサブクラス化され、抽象モデルのapp_modelと関連しなくなった場合、抽象モデルを利用したこの方法で定義されるリレーションは決められます。
products/models.py
from django.db import models class AbstractCar(models.Model): manufacturer = models.ForeignKey('Manufacturer', on_delete=models.CASCADE) class Meta: abstract = True
production/models.py
from django.db import models from products.models import AbstractCar class Manufacturer(models.Model): pass class Car(AbstractCar): pass # Car.manufacturer will point to `production.Manufacturer` here.
他のアプリケーションで定義されたモデルを参照するには、フルのアプリケーションラベルをつけてモデルを明示的に特定する必要があります。例えば、上記のManufacturerモデルが、productionという他のアプリケーションで定義される場合、下記のようにします。
class Car(models.Model): manufacturer = models.ForeignKey( 'production.Manufacturer', on_delete=models.CASCADE, )
遅延評価と呼ばれる※1この参照方法は、2つのアプリケーション間で回帰する依存関係?を解消します。
データベースのインデックスは自動的にForeignKeyに作成されます。db_indexをFalseにすることでこれを無効にすることができます。 一貫性のためにJOINではなくForeignKeyを作っている場合や、部分インデックスまたは複合インデックスのような代替的なインデックスを作成している場合、インデックスの重複を避けたくなるでしょう。
データベースでの表記方法
Djangoは裏で、データベースのカラム名を作る際にフィールド名の後ろに"_id"をつけます。上記の例では、Carモデルはmanufacturer_idカラムを持ちます。(db_columnを指定することで、明示的にこれを変更することができます。)しかし、カスタムSQLを書かない限り、コードでデータベースのカラム名を扱うべきではありません。常にモデルのオブジェクトのフィールド名を扱うべきです。
引数
ForeignKeyは、リレーションがどのように機能するかの詳細を定義した他の引数を受け付けます。
ForeignKey.on_delete
ForeingKeyで参照されているオブジェクトが削除された時、Djangoはon_delete引数によって指定されたSQL制約をエミュレートします。例えば、もしNULLにできるForeignKeyがあり、参照されたオブジェクトが削除された際にNullをセットしたいとします:
user = models.ForeignKey( User, models.SET_NULL, blank=True, null=True, )
on_deleteは、データベースにSQL制約を作成しません。データベースレベルのカスケードのオプションは後々実装される予定です。
on_deleteの設定可能な値は、django.db.modelsで確認できます:
カスケードの削除。DjangoはON DELETE CASCADE でSQL制約の振る舞いをエミュレートし、ForeignKeyを含むオブジェクトを削除します。
関連したモデルにModel.delete()は呼ばれませんが、削除されるすべてのオブジェクトにpre_deleteとpost_deleteの信号が送られます。
- PROTECT[ソース]
django.db.IntegrityErrorのサブクラスであるProtectedErrorを発生させることで、参照されたオブジェクトが削除されるのを防ぎます。
- SET_NULL[ソース]
ForeignKeyをNullにセットします; これはnullがTrueである場合のみ可能です。
- SET_DEFAULT[ソース]
ForeinKeyをデフォルト値にセットします; ForeinKeyのデフォルトを必ずセットしなくてはなりません。
- SET()[ソース]
ForeinKey、もしくはcallableが渡された場合はそれを呼び出した結果をSET()にに渡される値にセットします。ほとんどの場合、callableを渡すのは、models.pyをインポートした時にクエリーを実行することを避けるために必要です。
from django.conf import settings from django.contrib.auth import get_user_model from django.db import models def get_sentinel_user(): return get_user_model().objects.get_or_create(username='deleted')[0] class MyModel(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET(get_sentinel_user), )
- DO_NOTHING[ソース]
何も実行しません。もし、データベースバックエンドが参照整合性を強要する場合、ON DELETE制約をデータベースのフィールドに自動的に追加しない限り、 IntegrityErrorを引き起こします。
ForeignKey.limit_choices_to
ModelFormかadminを利用してこのフィールドがレンダーされている時に、選択可能な選択肢を制限します。(デフォルトでは、クエリーセットのすべてのオブジェクトを選択することができます。)辞書型、Qオブジェクト、辞書型かQオブジェクトを返すcallableが利用可能です。
staff_member = models.ForeignKey( User, on_delete=models.CASCADE, limit_choices_to={'is_staff': True}, )
例えば上記は、is_staff=TrueであるUsersしか選べないようなModelFormのフィールドを作成します。これはDjango adminに役立ちます。
例えば、下記のように、 日付の選択肢を制限するためにPythonのdatetimeモジュールと接続して利用する時にcallableフォームも役立ちます。
def limit_pub_date_choices(): return {'pub_date__lte': datetime.date.utcnow()} limit_choices_to = limit_pub_date_choices
limit_choices_toがQオブジェクトである、もしくはQオブジェクトを返す場合(Qオブジェクトは、複雑なクエリーに役立ちます)、limit_choices_toがModelAdminのraw_id_fieldsのリストに入っていない時にadminで利用できる選択肢に影響します。
注釈
limit_choices_toがcallableに利用されている場合、新しいフォームが初期化されるたびに呼び出されます。また、例えばmanageコマンドやadminによってモデルが有効にされた時にも呼び出されます。adminは何回も、多様なエッジケースにおいてフォームの入力を有効化するためにクエリーセットを構築します。そのため、callableが何回も呼び出される可能性があります。
ForeignKey.related_name
あるオブジェクトと関連しているオブジェクトとの関係に利用する名前です。また、related_query_name(ターゲットとなるモデルからの逆引きされる名前に利用するための名前)に使うためのデフォルト値でもあります。)全ての説明と例を見るには、「リレーションシップ "反対向き” を理解する」をご参照ください。抽象モデルとの関係を定義する時はこの値をセットする必要があることを心に留めておいてください。それによって、いくつかの特別なシンタックスを利用できます。
Django裏側での関係を生成して欲しくない場合、related_nameに'+'や'+'で終わる名前をつけてください。例えば、これによってUserモデルがこのモデルと裏側での関係を持たないようにします。
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='+',
)
ForeignKey.related_query_name
ターゲットとなるモデルの逆引きに利用するための名前です。設定すると、related_nameまたはdefault_related_nameのデフォルトとなります。もしくはそのモデル名のデフォルトとなります。
# Declare the ForeignKey with related_query_name class Tag(models.Model): article = models.ForeignKey( Article, on_delete=models.CASCADE, related_name="tags", related_query_name="tag", ) name = models.CharField(max_length=255) # That's now the name of the reverse filter Article.objects.filter(tag__name="important")
related_nameのように、related_query_nameは特別なシンタックスを通してapp labelとクラスの補間をサポートします。
ForeignKey.to_field
関連しているオブジェクトのフィールドです。デフォルトでは、Djangoは関連したオブジェクトのプライマリーキーを利用します。異なるフィールドを参照したい場合、そのフィールドをunique=Trueにしなければなりません。
ForeignKey.db_constraint
そのForeignKeyの制約をデータベースに作るかどうかの制御をします。デフォルトは、Trueですが、ほぼあなたがしたいことでしょう。これをFalseにすることはデータの一貫性からして良くないです。ここで、これを行いたい場合のシナリオを用意しました。
妥当ではないレガシーなデータがある場合
データベースを破損した場合
Falseにセットした場合、存在しない関連したオブジェクトにアクセスしたらDoesNotExistを例外として吐き出します。
ForeignKey.swappable
ForeignKeyがスワップできるモデルを指している時のマイグレーションフレームワークの動きを制御します。True(デフォルト)の場合、ForeignKeyがsettings.AUTH_USER_MODEL(もしくは、他のスワップできるモデルに関するsettings)の現在の値と整合するモデルを指している場合、モデルに直接する保管するのではなく、関係はsettingsの参照を利用するマイグレーションに保管されます。
例えばモデルが常にスワップインされたモデル(例えば、カスタムユーザーモデルのために特別に設計されたプロフィールモデル)を指すと確信している場合のみ、Falseをオーバーライドしたくなるでしょう。
Falseにすることは、スワップできるモデルがスワップアウトされても参照できるという意味ではありません。Falseはただ、ForeignKeyと作られたマイグレーションはあなたが特定したモデルを常に参照することを意味します(例えば、あなたがサポートしていないUserモデルを実行しようとすると落ちます。)
不安な場合、デフォルトであるTrueのままにしておいてください。
※1 Django特有の他の言い方があるように思えるので合ってる自信がありません汗
【非公式翻訳】Google API Client Libraries > Python
公式の日本語版がなかったので、備忘録的に・・・
※2017/02/17が最終更新の記事となります。 ※表現するのが難しかったので図中の語句の訳は書きませんでした。 ※わかりづらい箇所や間違えている箇所がありましたら、コメント欄にてご指摘お願いします。
OAuth 2.0
このドキュメントはOAuth 2.0を説明してます。OAuth 2.0を利用する際に、どのようにクライアントIDを取得するのか、どのようにPythonのGoogle API クライアントのライブラリと利用するかを説明します。
OAuth 2.0とは
OAuth 2.0は、Google APIに利用される認証プロトコルです。このライブラリーのドキュメントの認証に関するページにようやくされています。また、他にも良い参照ページがあります。
プロトコルは複雑な問題を解決するため、理解するのが難しいかと存じます。
このプレゼンはプロトコルの重要な概念と、各ステップでライブラリをどのように利用するかを説明します。
ーーーーー
※プレゼンテーションスライドの翻訳。
(1枚目) OAuth 2.0とPythonのためのGoogle API クライアント
(2枚目) OAuth 2.0では、プロトコルが少々複雑です。
(3枚目) OAuth 2.0は、トリッキーな問題を解決しようとしています。
(4枚目) あなた(開発者)がアプリケーションをビルドしようとする際、
(5枚目) もしあなたのユーザーが
(6枚目) あなたのアプリケーションが機能するために必要な、他のサービス内にあるデータを持っているとすると
(7枚目) ー例えば、彼らのタスクリストや写真のような
(8枚目) どのようにそのデータを取得すればよいでしょうか?
(9枚目) あなたはユーザーに名前とパスワードを要求することができます。
(10枚目) しかし、そうしますと、ユーザーは そのサービス内にある彼らのデータすべてに対するアクセス権を あなたのアプリケーションに提供してしまいます。 それは安全ではありません。それはしないでください。
(11枚目) 名前とパスワードは彼らの電子上に保管されているすべての情報のキーのようなもので、それらをユーザーに要求すべきではありません。
(12枚目) 我々が本当に欲しているものは、APIの限られたデータへのみアクセスを認証する特別なキーです。
(13枚目) 特別なキーは、名前もパスワードも使わずに、アプリケーションを利用できるよう 要求することができます。
(14枚目) その仕組みが機能するためには、API・ユーザー・アプリケーションの全員が、「自分はーーーである」と述べている通りの者であるかをそれぞれ確かめなければなりません。
(15枚目) それが、この複雑さの由来となります。
(16枚目) 実は先程の説明よりももう少し複雑です。なぜなら、その特別なキーは
(17枚目) データをセキュアな状態に保つために何回も変化するからです。
(18枚目) ようやく OAuth 2.0がどのようなものかが分かりました。それはPythonのGoogle API クライアントではどのように機能するのでしょうか?
(19枚目) そのキーはCredentialsオブジェクトの中にあります
(20枚目) Credentialsを得るためにはFlowオブジェクトを通さなければなりません。
(21枚目) 最終的に、キーは何回も変化するため、キーを保持し、取り出すためのStorageオブジェクトがあります。
(22枚目) セットアップし、Flowオブジェクトを実行することでCredentialsオブジェクトを生成し、Storageオブジェクトに生成したCredentialsオブジェクトを保管します。
(23枚目) 後々、キーが必要になった時、Storageオブジェクトから取り出し、利用することができます。
(24枚目) この画像の図よりもシンプルだと思っていただけると嬉しいです。
(25枚目) 実際のコードを見てみましょう。
(26枚目) まず、Flowオブジェクトを生成します。
(27枚目) Google APIでは、あなたのアプリケーション用のクライアントIDと秘密鍵を作成するために
http://code.google.com/apis/console
にアクセスする必要があります。
(28枚目) Flowを立ち上げます。
(29枚目) Flowが終了した時に、Credentialsを取得します。それをStorageに保管します。
(30枚目) Credentialsを利用するために、Storageから取り出し、httplib2.Http()オブジェクトCredentialsを適用します。
(31枚目) httpで作られたあらゆるHTTPリクエストは、これらのCredentialsによって認証されます。
(32枚目) プラットフォームごとにStorageクラスがあります。
(33枚目) より簡単にするために、Appエンジンを助ける方法もあります。
(34枚目) デコレーターを載せると、デコレーターによってFlows, Storage, Credentialsが利用されます。
(35枚目) oauth_requiredは最もシンプルに利用できるインターフェースある一方、oauth_awareが推奨されているインターフェースです。
(36枚目) ユーザーのデータへのアクセスをリクエストしている理由の説明をユーザーに弁明するために、linkを設置することができます。
(37枚目) 以上が概要となります。より詳細な情報は下記をご参照ください。
より詳しい情報がWikiにあります。
https://developers.google.com/api-client-library/python/guide/aaa_oauth
説明したクラスに関するPyDocはこちら
※上記4つにアクセスしたところ、404エラーになってしまいました。
ーーーーー
クライアントIDと秘密鍵を取得する
Google APIs ConsoleのAPI Access paneでクライアントIDと秘密鍵を取得できます。異なる種類のクライアントIDがあるため、あなたのアプリケーションにとって正しい種類のものを取得するに気をつけましょう。
- WebアプリケーションのクライアントID
- インストールアプリケーションのクライアントID
- サービスアカウントのクライアントID
注意: 秘密鍵が外にもれないようにしてください。もし他の誰かがあなたの秘密鍵を取得したら、あなたのクォータを消費し、Google API Consoleプロジェクトの変更を突破し、ユーザーのデータにアクセスするようリクエストを送る可能性があります。
oauth2clientライブラリ
oauth2clientライブラリはPythonのGoogle API Client ライブラリに含まれています。それは、APIを呼ぶのに必要なOAuth 2.0のプロトコルのすべてのステップを扱います。OAuth 2.0ライブラリのみ必要である場合、分離したパッケージとして利用することも可能です。下記の段落では、このライブラリで重要なモジュール、クラス、機能を説明します。
Flows
Flowクラスの目的は、あなたのアプリケーションがユーザーのデータにアクセスすることを認証するための認証情報を得ることです。ユーザーがアクセスを認証するには、OAuth 2.0のステップはあなたのアプリケーションに、潜在的に複数回ブラウザにリダイレクトすることを要求します。Flowオブジェクトには、あなたのアプリケーションがこれらのステップを踏んで認証情報を得ることを助ける機能があります。Flowオブジェクトは一時的なものであり、Credentialsを生成すると捨てられる可能性があります。ただし、それらは保存(pickle)されたり保管されることもできます。
★メモ:プラットフォーム特有のFlowのための「Google App Engineを利用する 」と「Djangoを利用する」のページをご覧ください。
flow_from_clientsecrets()
oauth2client.client.flow_from_clientsecrets() メソッドは、 client_secrets.jsonファイルからFlowオブジェクトを生成します。この形式のJSONファイルはクライアントID、秘密鍵、OAuth 2.0のパラメータを保管しています。
下記は、Flowオブジェクトを作成するためにどのようにflow_from_clientsecrets() を利用するかを示します。
from oauth2client.client import flow_from_clientsecrets ... flow = flow_from_clientsecrets('path_to_directory/client_secrets.json', scope='https://www.googleapis.com/auth/calendar', redirect_uri='http://example.com/auth_return')
OAuth2WebServerFlow
名前にそぐわず、oauth2client.client.OAuth2WebServerFlow クラスはイントールアプリケーションにもWebアプリケーションにも利用できます。OAuth2WebServerFlowオブジェクトは、クライアントID・秘密鍵・そのコンストラクタへのスコープをパスすることで生成されます。: あなたはredirect_uriパラメータでコンストラクタを提供します。これはあなたのアプリケーションが扱っているURIでなければなりません。
from oauth2client.client import OAuth2WebServerFlow ... flow = OAuth2WebServerFlow(client_id='your_client_id', client_secret='your_client_secret', scope='https://www.googleapis.com/auth/calendar', redirect_uri='http://example.com/auth_return')
step1_get_authorize_url()
Flowクラスのstep1_get_authorize_url() ファンクションは認証サーバのURIを生成するために利用されます。 認証サーバのURIをもつと、ユーザーがそこにリダイレクトします。下記は、このファンクションを呼び出す一例です。
auth_uri = flow.step1_get_authorize_url()
# Redirect the user to auth_uri on your platform.
ユーザーが以前にあなたのアプリケーションへのアクセスが認証されていたら、認証サーバはただちにredirect_uriにリダイレクトします。ユーザーがまだアクセスが認証されていなかったら、認証サーバはあなたのアプリケーションにアクセスしても良いか訪ねます。アクセスを認証したら、下記のようなcodeクエリの文字列パラメータをもったredirect_uriにリダイレクトします。
http://example.com/auth_return/?code=kACAH-1Ng1MImB...AA7acjdY9pTD9M
アクセスを拒否したら、下記のようなエラークエリの文字列パラメータをもったredirect_uriにリダイレクトします。
http://example.com/auth_return/?error=access_denied
step2_exchange()
Flowクラスのstep2_exchange() ファンクションは、Credentialsオブジェクトの認証コードを交換します。このファンクションに認証サーバのリダイレクトで得たコードを渡してください。
credentials = flow.step2_exchange(code)
Credentials
Credentialsオブジェクトは、単一のユーザーデータへのアクセスを認証するリフレッシュトークンとアクセストークンを保持します。これらのオブジェクトは、アクセスを認証するhttplib2.Httpオブジェクトに適用されます。一度だけ適用すればよく、それらは保管されることができます。この段落ではCredentialsオブジェクトを 生成・利用するための様々なメソッドを説明します。
★メモ:プラットフォーム特有のFlowのための「Google App Engineを利用する 」と「Djangoを利用する」のページをご覧ください。
OAuth2Credentials
oauth2client.client.OAuth2Credentials クラスは、ユーザーのデータへのアクセスを認証する、OAuth 2.0の認証情報をもっています。通常、コンストラクタを呼んでこのオブジェクトを生成しません。Flowオブジェクトが生成します。
ServiceAccountCredentials
oauth2client.service_account.ServiceAccountCredentialsクラスは、OAuth 2.0 Service Accountsでしか利用されません。エンドユーザーはサーバー間のAPIの呼び出しに関わりません。そのため、Flowオブジェクトなしで直接このオブジェクトを生成することができます。
AccessTokenCredentials
oauth2client.client.AccessTokenCredentialsクラスは他の方法によってアクセストークンを既に取得している時に利用されます。Flowオブジェクトなしでこのオブジェクトを生成できます。
authorize()
必要な認証ヘッダーをhttplib2.Httpインスタンスによって作成されたすべてのリクエストへに適用するには、 Credentialsクラスのauthorize()ファンクションを利用してください。:
import httplib2
...
http = httplib2.Http()
http = credentials.authorize(http)
一度httplib2.Httpが認証されたら、それは一般的にビルドファンクションに渡されます。
from apiclient.discovery import build ... service = build('calendar', 'v3', http=http)
Storage
oauth2client.client.Storageオブジェクトは、Credentialsオブジェクトを保管し、取り出します。この段落では、Storageオブジェクトを生成・利用する様々なメソッドを説明します。
★メモ:プラットフォーム特有のFlowのための「Google App Engineを利用する 」と「Djangoを利用する」のページをご覧ください。
file.Storage
oauth2client.file.Storageクラスは単一のCredentialsオブジェクトを保管し、取り出します。このクラスは複数のプロセスやスレッドが単一のストアをオペレーションできるようなロックをサポートします。下記は、どのようにファイルを開き、Credentialsをファイルに保管し、これらの認証情報を取得するのかを示します。
from oauth2client.file import Storage ... storage = Storage('a_credentials_file') storage.put(credentials) ... credentials = storage.get()
multistore_file
oauth2client.contrib.multistore_file モジュールを利用すると、複数の認証情報を保管することができます。認証情報は下記によって識別されます。
- クライアントID
- ユーザー情報
- スコープ
keyring_storage
oauth2client.contrib.keyring_storage モジュールは、パスワードマネージャーを利用できる場合、単一のCredentialsオブジェクトをパスワードマネージャーの中に保管させることができます。認証情報は下記によって識別されます。
- クライアントアプリケーションの名前
- ユーザーネーム
from oauth2client.contrib.keyring_storage import Storage ... storage = Storage('application name', 'user name') storage.put(credentials) ... credentials = storage.get()
Command-line tools
oauth2client.tools.run_flow() ファンクションは、コマンドラインアプリケーションが認証情報を取得するために利用されます。Flow引数は、ユーザーのデフォルトのWebブラウザーにある認証サーバのページを開こうとします。認証サーバはユーザー にあなたのアプリケーションがユーザーのデータにアクセスしてよいか訪ねます。もしユーザーがアクセスを許可したら、run()ファンクションが新しい認証情報を返します。新しい認証情報はStorage引数に保管され、そのStorageオブジェクトに関連したフィアルをアップデートします。
oauth2client.tools.run_flow()ファンクションは、コマンドラインのフラグによって制御されます。Pythonの標準ライブラリのargparse モジュールは、あなたのプログラムの最初に初期化されなくてはなりません。argparseはPython 2.7以上に含まれており、それ以前のバージョンではseparate packageとして利用可能です。下記は、どのようにこのファンクションを利用するかを示します。
import argparse from oauth2client import tools parser = argparse.ArgumentParser(parents=[tools.argparser]) flags = parser.parse_args() ... credentials = tools.run_flow(flow, storage, flags)
特に記載のない限り、このページのコンテンツは クリエイティブ・コモンズの表示 3.0 ライセンス により使用許諾されます。サンプル コードは Apache 2.0 ライセンス により使用許諾されます。詳しくは、Google のサイトに関するポリシーをご覧ください。JavaはOracleのアフィリエイトのトレードマークに登録されています。
最終更新日 2017年2月17日