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日
2019/01/26オブジェクト指向分析会に参加しました
なんとなくオブジェクト指向をサポートしているプログラミング言語を書いてるけどちゃんと概念を学んだことがないなと思い、参加してみました!
今回の趣旨
オブジェクト指向に則ってモデリングしてみて、オブジェクト指向に則って実装してみよう!
やったこと
おこづか帳のモデリング・実装をしました。
1. モデリング
おこづかい帳に必要な要素(金額、項目、日付など)をいったんブレインストーミング的に書き出す
↓
必要な要素をリファクタリング
「収入」と「支出」という項目があるけど、それぞれ別の項目にするか・それとも「収支」としてその中に金額を入力するかなど。
↓
ユーザーを想定し、必要な要素を絞り込む
今回は、小学生が利用するようなおこづかい帳を想定し、複数口座の利用や前借りなどの概念はいれませんでした。
↓
要素のクラスタリング
各要素を
値・集合・ロジック
に分類しました。
他の人も振り返りで仰っていたのですがユースケースをしっかり固める時間があったら更に良かったなと思いました。
何が必要で何がいらないかがより明確になるので。
2.実装
今回は、主催者の方がJavaマスターということもあり、Javaで実装しました。
参加者6人でワイワイモブプロしました。これほどの人数でモブするのは初めてでしたが、質問しやすい雰囲気で着いてこれて嬉しかったです。
学んだこと
1 値・集合・ロジックとは
モデルは要素によって成り立っています。要素は主に、値・集合・ロジックに分類されます。
「値」は、一つの特徴しか持っていない要素です。例えば、「名前」という要素しか持っていない「科目」という要素。
「集合」は、複数の「値」から構成される要素です。例えば、お小遣いの収入または支出を表す「取引」は、「日付」「科目」「収支金額」の複数の「値」から成立します。
「ロジック」は、処理を表します。例えば、科目ごとの収支金額を計算する処理は「ロジック」になります。科目ごとの収支金額を計算する処理によって返される金額は「値」になります。
「値」と「集合」を分けることによって、どのオブジェクトに何が必要か・何から成り立っているかが整理されるのは良いなと思いました。
また、いつもメンバ変数込みのクラスの中にロジックも書くのですが、 「ロジック」をいったんわけ、後から「誰がその処理の主語となるのか」を考えられるのも、分かりやすいコードを書く上で良いと思いました。
2. インターフェースがあったら嬉しいこと
Pythonにはインターフェースがないので、いまいち分かっていなかったのですが、 インターフェースを継承すると、そのインターフェースに書かれている振る舞いを強制的にもつことになり あやまった振る舞いをしなくなるといったような利点があるようです。
また、そのオブジェクト特有の細かい振る舞いが決まっていなくても、インターフェースを継承すれば実装・テストできるという利点もあるそうです。
よかったこと
1. 「一人ひとり感想を書く→他の人の感想を紹介する」というスタイルの振り返り
普段は、一人ひとりが感想を順に言っていくことが多いですが・・・
ホワイトボードに個々に感想を書いて、インタビュー形式で他の人の感想を紹介するという方法で振り返りました!
対話を通して他の人の思考を学べるし、話を遮られることがないのでとても良かったです!
2. モブだからこそ学べる豊富なボキャブラリー
要素の名前を決める時、一つの要素に対していろんな言葉の案が出てきました。
一人でプログラミングする時はその場で思いついた名前をつけることが多いので、言葉について深く考える機会になって良かったなと思います。
また、どの要素も、その要素の特徴を最も端的に示す言葉が選ばれた印象でした。どの言葉を利用するかを決める作業を通して、その要素の特徴を可視化することが出来た気がします。
逆に、言葉がいまいち決まりきらなかった要素に関しては、「やっぱいらないね」となったり使い方が決まらなかったりして
要素の概念の理解の名前付けはつながっているなあと思いました。
概念に困ったらどんな言葉で表すか考え直すアプローチもありかもしれません。
3. 主催の方が淹れてくれた美味しいコーヒー
ほんっっっとうに美味しかったです! その場でハンドドリップコーヒーを淹れてくださりました!
なごむし会話が生まれるし、コーヒーは正義だと改めて実感しました。
今度は私もコーヒー淹れたいです(*>▽<*)
20181103 ミニTDDBC振り返りその3
だいぶ放置してしまいましたが・・・(^^;
の続きです。
※今回、DDDの「Value Object」が出てきますが、
DDD学び始めたばかりで理解が曖昧なので
違ったらご指摘いただけると幸いです(>_<)
今回学んだことは
1. きれいなコミットメッセージの書き方
2. 学習テスト
3. DDDなリファクタリング
この記事では、「3. DDDなリファクタリング」について書きます。
今回の題材はこちら
我々のGitHub
3. DDDなリファクタリング
問3が終わったところで、
メジャーバージョン・マイナーバージョン・パッチバージョンには
「0以上の整数である」
という共通した特徴があるという話になりました。
この時点のプロダクトコード↓
class SemVer: def __init__(self, major: int, minor: int, patch: int) -> None: if major < 0: raise ValueError("メジャーバージョンは0以上") if minor < 0: raise ValueError("マイナーバージョンは0以上") if patch < 0: raise ValueError("パッチバージョンは0以上") self.major = major self.minor = minor self.patch = patch def get_notation(self) -> str: return str(self.major) + "." + str(self.minor) + "." + str(self.patch) def __eq__(self, other) -> bool: return self.get_notation() == other.get_notation() \ and isinstance(other, SemVer)
テストコード↓
class TestSemVer(unittest.TestCase): def test_major_minor_patchにそれぞれ1と4と2を与えてバージョンオブジェクトを作成(self): self.assertEqual("1.4.2", SemVer.generate(1, 4, 2).get_notation()) def test_2つのSemVerオブジェクトの等価性を比較できる(self): with self.subTest("等しい場合"): self.assertTrue(SemVer.generate(1, 4, 2) == SemVer.generate(1, 4, 2)) with self.subTest("異なる場合"):
そこで、int型ではなく、
「0以上の整数である」という特徴を持つ「VersionNumer」クラスを作成し
それぞれをVersionNumber型にしようということになりました。
major, minor, patchがVersionNumberクラスで管理されるような特徴をもつように、
同じ属性を保持し、生成されてから変化しないオブジェクトをValue Objectというそうです。
追加したテストコード↓
def test_バージョン番号は0以上の整数であること(self): self.assertRaises(ValueError, lambda: VersionNumber(-1))
プロダクトコードにVersionNumberクラスを追加します。
class VersionNumber: def __init__(self, value: int): if value < 0: raise ValueError("バージョン番号は0以上の整数でなければなりません") self.value = value
ただ、この時点では、他のコードはそれぞれのナンバーをint型として扱っているので他のテストが落ちます。
他のテストを通すために、一度
VersionNumberでSemverを生成するためのテストを追加します。
def test_オブジェクトを生成しやすくする(self): self.assertEqual("1.4.2", SemVer.generate(1, 4, 2).get_notation())
直したプロダクトコード↓
class SemVer: def __init__(self, major: VersionNumber, minor: VersionNumber, patch: VersionNumber): self.major = major self.minor = minor self.patch = patch def get_notation(self) -> str: return str(self.major.value) + "." + str(self.minor.value) + "." + str(self.patch.value) def __eq__(self, other) -> bool: return self.get_notation() == other.get_notation() \ and isinstance(other, SemVer) @classmethod def generate(cls, major: int, minor: int, patch: int): return SemVer(VersionNumber(major), VersionNumber(minor), VersionNumber(patch))
まず、generateメソッドを作り、その引数にintを渡します。
intをもとに、VersionNumberを要素にしたSemverオブジェクトを生成します。
generateメソッドが完成した後に、
他のメソッドを書き直しました。
Value Objectとして管理し、特徴をまとめたクラスを作り、そのクラスのオブジェクトとすることで、
3つ別々にテストせず
特徴をまとめたクラスのみテストすれば良くなる利点があります。
後々に修正する際に楽ですね!
また、あとでコードを見返す時に
3つのオブジェクトが共通した特徴をもつことがひと目で分かることも良いです。
この学びは、DDDを学んでいらっしゃる方とのモブプロをしないとありませんでした!
ペアプロ・モブプロは最高ですね(^0^)/
【Django】すべてをviews.pyのみに書いていたのをdomain.pyを作ってリファクタリングした
DB関連の処理も、計算関連の処理のための関数も、全てviewsに書いていましたが、
viewsに全てを書くのをやめました!
という話です。
動くからいいやん!と思っていましたが、viewsにはviewsの役割があり、上記の処理はviewsでやることではないとか。
半年ほど前に下書きのまま放置していたのをようやく投稿(^^;
viewsやmodeslなど、それぞれどんな役割か?
ざっくり、forms・models・viewsの役割はこんな感じ。
- forms
フォームの入力項目やバリデーションを管理します。
- models
データベースに格納するデータを管理します。
テーブルとかカラムとか、データベース設計はmodels.pyを見れば把握できます。
- views
リクエストやリスポンスにまつわる処理を管理します。
送られてきたリクエストをもとに、どのような内容を表示させるかを決定をしています。
CodeBeforeAfter
ユーザー登録機能を例にあげます。
過去記事のです(過去の未熟さを積極的にさらすスタイルでいきます笑)
まず、forms、models、viewsだけのコードがこちら↓
forms.py
from django import forms from django import forms class RegistrationForm(forms.Form): username = forms.CharField() password = forms.CharField() email = forms.EmailField()
models.pyは、ユーザー登録に関してはUserクラスをインポートすればmodelsに書かなくても良いので割愛。
views.py
import django.http import myApp.forms from django.shortcuts import render import uuid from django.contrib.auth.models import User import re def has_digit(text): if re.search("\d", text): return True return False def has_alphabet(text): if re.search("[a-zA-Z]", text): return True return False def registation_user(request): if request.method == 'POST': registration_form = myApp.forms.RegistrationForm(request.POST) password = request.POST['password'] if len(password) < 8: registration_form.add_error('password', "文字数が8文字未満です。") if not has_digit(password): registration_form.add_error('password',"数字が含まれていません") if not has_alphabet(password): registration_form.add_error('password',"アルファベットが含まれていません") if registration_form.has_error('password'): return render(request, 'registration.html', {'registration_form': registration_form}) user = User.objects.create_user(username=request.POST['username'], password=password, email=request.POST['email']) return django.http.HttpResponseRedirect('/login') else: registration_form = myApp.forms.RegistrationForm() return render(request, 'registration.html', {'registration_form': registration_form})
そのままでも動きますが、
viewsに登録や判定など
表示に関係ない部分もまとめて書いています。
コードが長くなると、どこでどの処理を行っているのかを把握するのが困難になります。
また、viewsが長すぎて悪い部分を修正するのが大変になります。
そこで、domains.pyを作成し、
viewにはリクエストをもとに表示内容を決める処理を書き、
domainsは計算などのそのシステムの中核となる処理を書くようにします。
役割を分けて書いた結果(viewsとdomains以外は変わっていないのでforms.pyは割愛)↓
domains.py
def post_new_post(author: User, post: Dict[str, Any], file: MultiValueDict) -> None: if not author.is_authenticated: raise minsta.exceptions.LoginRequiredError() form = minsta.forms.NewPostForm(post, file) if not form.is_valid(): raise minsta.exceptions.ValidationError(form) file_path = handle_uploaded_file(form.cleaned_data['file']) print(type(file)) minsta.models.Post.create(author, file_path, form.cleaned_data['comment']) def handle_uploaded_file(f): file_path = "uploads/{}.jpeg".format(uuid.uuid4().hex) with open('minsta/static/{}'.format(file_path), 'wb') as destination: # 'minsta/static/uploads/{}.jpeg'.format(uuid.uuid4().hex) .format()で{}の中を()の中に置き換えてくれる # uuidとは、ランダムでユニークなIDの規格である。uuid.uuid4().hexの.hexで文字列にする。 for chunk in f.chunks(): destination.write(chunk) return file_path
views.py
import django.http import minsta.models import minsta.forms from django.shortcuts import render from django.contrib.auth.models import User import minsta.domain def get_post_new_post(request): try: minsta.domain.check_authenticated(request.user) except minsta.exceptions.LoginRequiredError: return django.http.HttpResponseRedirect('/login') form = minsta.forms.NewPostForm() return render(request, 'new_post.html', {'form': form, 'user':request.user}) def post_post_new_post(request): try: minsta.domain.post_new_post(request.user, request.POST, request.FILES) except minsta.exceptions.LoginRequiredError: return django.http.HttpResponseRedirect('/login') except minsta.exceptions.ValidationError as e: return render(request, 'new_post.html', {'form': e.form, 'user':request.user}) return django.http.HttpResponseRedirect('/list') def post_new_post(request): if request.method == 'POST': return post_post_new_post(request) return get_post_new_post(request)
viewsとdomainsを分けると、
修正が必要になった際に
viewsとdomainsのどちらが悪いのかが分かり
悪い方だけ修正すれば良くなるので
修正範囲が小さくなり、修正がより楽になるのでおすすめです。