mizzsugar’s blog

Pythonで学んだことや読書録を書きます。

DjangoCongress2019に参加しました

初DjangoConでした!

とても刺激になったので拝聴したトークを自分なりにアウトプットしようと思います。

なお、DjangoCongressJPのHPは以下になります。(TimeTableも載っています)

djangocongress.jp


私が拝聴したのは下記のトークです!

  1. Djangoで静的ファイルとうまくやる話

  2. How to build and deploy a flexible React/Django hybrid application

  3. Djangoでのメール送信 - 設定からテストまで

  4. Djangoアプリのデプロイに関するプラクティス

  5. Authorization in Django

  6. DjangoによるWebエンジニア育成への道


Djangoで静的ファイルとうまくやる話

tell-k.github.io

staticファイルの扱いはなんとなくやっているところがあったので 丁寧に説明してくださってとても為になりました。

Django特有の話だけでなく

「静的」と「動的」の違いやNginxの設定の話もしてくださりました。

Django始めたときに出会いたかった・・・笑

また、AmazonS3を使うと静的ファイルの同期作業から解放されるとのことなので使いたさが増しました!


How to build and deploy a flexible React/Django hybrid application

slides.com

一部のページではDjangoテンプレートを使い、複雑なUIのページにはReactを使うというお話。

propsをrenderを使って返すという方法は聞いたことなかったので新鮮でした!

今までTemplateしか使っていなかったプロジェクトにReactやVueを導入する際に参考にしたいとも思いました。


Djangoでのメール送信 - 設定からテストまで

speakerdeck.com

Djangoのメール送信機能の仕組みのお話。

基本的な機能の説明はさながら、メール送信のユニットテストの方法やログ出力の方法までご紹介くださりました。

恥ずかしながら、実際にメール送信して挙動確認していたので

ユニットテストでメール文や添付ファイルの確認をする方法は早速仕事で使いたいと思います!

また、運用に備えてログの設定もしたい・・・!


Djangoアプリのデプロイに関するプラクティス

www.slideshare.net

DjangoというよりWeb全般という感じでしたが、構成管理から監視まで幅広くお話してくださり、貴重な資料だなと思いました。 すごすぎて途中から理解できない部分があったので、経験を重ねてからもう一度読み返したいです。

知識と実践を重ねの上に重ねた人なんだな、と思いました。


Authorization in Django

GitPitch Slide Deck

Djangoで複雑な認証を実装するときに役に立つライブラリ「djnago-keeper」の紹介でした。 (グループAの人は○○が出来き、グループBの人は○○と△△が出来る、みたいなやつ)

なんと、発表者であるhirokikyさんご自信が作ったとのこと・・・! すごい・・・

認証対象のユーザーモデルに__acl__メソッドを書き、その中に複雑なロジックを書けば

Viewデコレータをシンプルに保てるというのは便利と思いました。また、変更も__acl__のみで済むのも魅力的でした。


DjangoによるWebエンジニア育成への道

speakerdeck.com

エンジニアリング要素たっぷりというより、とある会社のストーリーという発表でした。

売上至上主義な開発で会社が行き詰まったところから、Pythonに強く付加価値のある開発が出来る会社に成長するまでに歩んだ道という内容です。

個人的に現状行き詰まっているところに重なり響くものが多く、とても勇気をもらいました。

懇親会で発表者のnakazawaさんとお話しましたが、気さくで良い方でした。たくさんアドバイスいただきました。ありがとうございます・・・!


たくさん刺激をもらった一日でした。Djangoについて知らないことを学ぶだけでなく、様々な人とお話してつながりを持てた貴重な機会でした。

初めてあったのに相談に乗ってくださった方もいて、とても感謝しています。

スタッフの皆様、素晴らしい機会を提供してくださりありがとうございました。

来年はトークorLTで登壇できるようにがんばるぞー!

【Django】JsonResponseでのテストの仕方(GET/POST)

久々の投稿となってしまいました(^^;


今回サンプルとしてテストしたいメソッドはこちらの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

  • リクエストで送った内容の商品が登録される

  • 想定しないメソッドでリクエストが送られたら405エラーとなる


まず、get_itemのテストを。

商品情報が返されるか確認します。

JsonResponseの中身(dataの部分)が正しいか確認したいということで探していると dataの部分はresponse.contentとなっていることがわかりました。

>>> from django.http import JsonResponse
>>> response = JsonResponse({'foo': 'bar'})
>>> response.content
b'{"foo": "bar"}'

docs.djangoproject.com

しかし、contentはbytes型なのでそのままではテストできません。

調べたら、json.loads()によってcontentをJSON化して内容をテストしている事例がありました。

medium.com

結果、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()

docs.djangoproject.com


続きましては、get_itemの異常系のテストです。

意図したステータスコードが返されるか確認します。

django.test.Clientによって作成されたdjango.test.Responseオブジェクトはアトリビュートstatus_codeにステータスコードを保持します。

response.status_codeでそのレスポンスのステータスコードがわかります。

docs.djangoproject.com

    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でリクエストが送られなかった模様。

stackoverflow.com

デフォルトのcontent_typeがMULTIPART_CONTENTなので、そりゃ何も指定しないでうまくいくはずがありませんでした。

docs.djangoproject.com

結果、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の日本語訳です。

docs.djangoproject.com

間違いがありましたら、コメント欄にてご指摘お願いします!


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_indexFalseにすることでこれを無効にすることができます。 一貫性のためにJOINではなくForeignKeyを作っている場合や、部分インデックスまたは複合インデックスのような代替的なインデックスを作成している場合、インデックスの重複を避けたくなるでしょう。


データベースでの表記方法

Djangoは裏で、データベースのカラム名を作る際にフィールド名の後ろに"_id"をつけます。上記の例では、Carモデルはmanufacturer_idカラムを持ちます。(db_columnを指定することで、明示的にこれを変更することができます。)しかし、カスタムSQLを書かない限り、コードでデータベースのカラム名を扱うべきではありません。常にモデルのオブジェクトのフィールド名を扱うべきです。


引数

ForeignKeyは、リレーションがどのように機能するかの詳細を定義した他の引数を受け付けます。

ForeignKey.on_delete

ForeingKeyで参照されているオブジェクトが削除された時、Djangoon_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 CASCADESQL制約の振る舞いをエミュレートし、ForeignKeyを含むオブジェクトを削除します。

関連したモデルにModel.delete()は呼ばれませんが、削除されるすべてのオブジェクトにpre_deletepost_deleteの信号が送られます。


django.db.IntegrityErrorのサブクラスであるProtectedErrorを発生させることで、参照されたオブジェクトが削除されるのを防ぎます。


ForeignKeyをNullにセットします; これはnullTrueである場合のみ可能です。


ForeinKeyをデフォルト値にセットします; ForeinKeyのデフォルトを必ずセットしなくてはなりません。

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),
    )


何も実行しません。もし、データベースバックエンドが参照整合性を強要する場合、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_toQオブジェクトである、もしくはQオブジェクトを返す場合(Qオブジェクトは、複雑なクエリーに役立ちます)、limit_choices_toがModelAdminraw_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(デフォルト)の場合、ForeignKeysettings.AUTH_USER_MODEL(もしくは、他のスワップできるモデルに関するsettings)の現在の値と整合するモデルを指している場合、モデルに直接する保管するのではなく、関係はsettingsの参照を利用するマイグレーションに保管されます。

例えばモデルが常にスワップインされたモデル(例えば、カスタムユーザーモデルのために特別に設計されたプロフィールモデル)を指すと確信している場合のみ、Falseをオーバーライドしたくなるでしょう。

Falseにすることは、スワップできるモデルがスワップアウトされても参照できるという意味ではありません。Falseはただ、ForeignKeyと作られたマイグレーションはあなたが特定したモデルを常に参照することを意味します(例えば、あなたがサポートしていないUserモデルを実行しようとすると落ちます。)

不安な場合、デフォルトであるTrueのままにしておいてください。


※1 Django特有の他の言い方があるように思えるので合ってる自信がありません汗

【非公式翻訳】Google API Client Libraries > Python

公式の日本語版がなかったので、備忘録的に・・・

※2017/02/17が最終更新の記事となります。 ※表現するのが難しかったので図中の語句の訳は書きませんでした。 ※わかりづらい箇所や間違えている箇所がありましたら、コメント欄にてご指摘お願いします。

developers.google.com


OAuth 2.0

このドキュメントはOAuth 2.0を説明してます。OAuth 2.0を利用する際に、どのようにクライアントIDを取得するのか、どのようにPythonGoogle 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がどのようなものかが分かりました。それはPythonGoogle 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はこちら

OAuth2WebServerFlow

Credentials

StorageByKeyName

OAuth2Decorator

※上記4つにアクセスしたところ、404エラーになってしまいました。

ーーーーー


クライアントIDと秘密鍵を取得する

Google APIs ConsoleのAPI Access paneでクライアントIDと秘密鍵を取得できます。異なる種類のクライアントIDがあるため、あなたのアプリケーションにとって正しい種類のものを取得するに気をつけましょう。

  • WebアプリケーションのクライアントID
  • インストールアプリケーションのクライアントID
  • サービスアカウントのクライアントID


注意: 秘密鍵が外にもれないようにしてください。もし他の誰かがあなたの秘密鍵を取得したら、あなたのクォータを消費し、Google API Consoleプロジェクトの変更を突破し、ユーザーのデータにアクセスするようリクエストを送る可能性があります。


oauth2clientライブラリ

oauth2clientライブラリはPythonGoogle 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 のサイトに関するポリシーをご覧ください。JavaOracleアフィリエイトのトレードマークに登録されています。

最終更新日 2017年2月17日

2019/01/26オブジェクト指向分析会に参加しました

なんとなくオブジェクト指向をサポートしているプログラミング言語を書いてるけどちゃんと概念を学んだことがないなと思い、参加してみました!

今回の趣旨

オブジェクト指向に則ってモデリングしてみて、オブジェクト指向に則って実装してみよう!

やったこと

おこづか帳のモデリング・実装をしました。


1. モデリング

おこづかい帳に必要な要素(金額、項目、日付など)をいったんブレインストーミング的に書き出す

必要な要素をリファクタリング

「収入」と「支出」という項目があるけど、それぞれ別の項目にするか・それとも「収支」としてその中に金額を入力するかなど。

ユーザーを想定し、必要な要素を絞り込む

今回は、小学生が利用するようなおこづかい帳を想定し、複数口座の利用や前借りなどの概念はいれませんでした。

要素のクラスタリング

各要素を

値・集合・ロジック

に分類しました。

f:id:mizzsugar:20190126224949j:plain
ホワイトボードに要素の関係性を図式化しました。値・集合・ロジックで色分けマーク分けするのはパッと理解できてよかったです。

他の人も振り返りで仰っていたのですがユースケースをしっかり固める時間があったら更に良かったなと思いました。

何が必要で何がいらないかがより明確になるので。


2.実装

今回は、主催者の方がJavaマスターということもあり、Javaで実装しました。

参加者6人でワイワイモブプロしました。これほどの人数でモブするのは初めてでしたが、質問しやすい雰囲気で着いてこれて嬉しかったです。

学んだこと

1 値・集合・ロジックとは

モデルは要素によって成り立っています。要素は主に、値・集合・ロジックに分類されます。

「値」は、一つの特徴しか持っていない要素です。例えば、「名前」という要素しか持っていない「科目」という要素。

「集合」は、複数の「値」から構成される要素です。例えば、お小遣いの収入または支出を表す「取引」は、「日付」「科目」「収支金額」の複数の「値」から成立します。

「ロジック」は、処理を表します。例えば、科目ごとの収支金額を計算する処理は「ロジック」になります。科目ごとの収支金額を計算する処理によって返される金額は「値」になります。


「値」と「集合」を分けることによって、どのオブジェクトに何が必要か・何から成り立っているかが整理されるのは良いなと思いました。

また、いつもメンバ変数込みのクラスの中にロジックも書くのですが、 「ロジック」をいったんわけ、後から「誰がその処理の主語となるのか」を考えられるのも、分かりやすいコードを書く上で良いと思いました。


2. インターフェースがあったら嬉しいこと

Pythonにはインターフェースがないので、いまいち分かっていなかったのですが、 インターフェースを継承すると、そのインターフェースに書かれている振る舞いを強制的にもつことになり あやまった振る舞いをしなくなるといったような利点があるようです。

また、そのオブジェクト特有の細かい振る舞いが決まっていなくても、インターフェースを継承すれば実装・テストできるという利点もあるそうです。


よかったこと

1. 「一人ひとり感想を書く→他の人の感想を紹介する」というスタイルの振り返り

普段は、一人ひとりが感想を順に言っていくことが多いですが・・・

ホワイトボードに個々に感想を書いて、インタビュー形式で他の人の感想を紹介するという方法で振り返りました!

対話を通して他の人の思考を学べるし、話を遮られることがないのでとても良かったです!


2. モブだからこそ学べる豊富なボキャブラリー

要素の名前を決める時、一つの要素に対していろんな言葉の案が出てきました。

一人でプログラミングする時はその場で思いついた名前をつけることが多いので、言葉について深く考える機会になって良かったなと思います。

また、どの要素も、その要素の特徴を最も端的に示す言葉が選ばれた印象でした。どの言葉を利用するかを決める作業を通して、その要素の特徴を可視化することが出来た気がします。

逆に、言葉がいまいち決まりきらなかった要素に関しては、「やっぱいらないね」となったり使い方が決まらなかったりして

要素の概念の理解の名前付けはつながっているなあと思いました。

概念に困ったらどんな言葉で表すか考え直すアプローチもありかもしれません。


3. 主催の方が淹れてくれた美味しいコーヒー

ほんっっっとうに美味しかったです! その場でハンドドリップコーヒーを淹れてくださりました!

f:id:mizzsugar:20190126220920j:plain
やなか珈琲さんのキリマンジャロ(中深煎り・中挽き)。苦めだけどほんのりナッツのような香ばしさと甘さがあり、チョコレートと相性ぴったりでした!

f:id:mizzsugar:20190126221128j:plain
ハンドドリップしてくださりました! 淹れてくださっている最中に香りも堪能できて幸せです!

なごむし会話が生まれるし、コーヒーは正義だと改めて実感しました。

今度は私もコーヒー淹れたいです(*>▽<*)

20181103 ミニTDDBC振り返りその3

だいぶ放置してしまいましたが・・・(^^;

mizzsugar.hatenablog.com

の続きです。

※今回、DDDの「Value Object」が出てきますが、

DDD学び始めたばかりで理解が曖昧なので

違ったらご指摘いただけると幸いです(>_<)


今回学んだことは

1. きれいなコミットメッセージの書き方

2. 学習テスト

3. DDDなリファクタリング


この記事では、「3. DDDなリファクタリング」について書きます。

今回の題材はこちら

お題: セマンティック・バージョニング · GitHub


我々のGitHub

github.com


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にそれぞれ142を与えてバージョンオブジェクトを作成(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というそうです。

little-hands.hatenablog.com


追加したテストコード↓

    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

ユーザー登録機能を例にあげます。

過去記事のです(過去の未熟さを積極的にさらすスタイルでいきます笑)

mizzsugar.hatenablog.com


まず、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のどちらが悪いのかが分かり

悪い方だけ修正すれば良くなるので

修正範囲が小さくなり、修正がより楽になるのでおすすめです。