Make組ブログ

Python、Webアプリや製品・サービス開発についてhirokikyが書きます。

Sentry with django-newauth

Sentry with django-newauth

djangonnewauth is a library to add customizable user model (It developed before Django 1.5). And sentry is a platform to collect logs and aggregate these.

Sentry can collect errors raised by some django applications, and displays informations of these errors.

Problem

Sentry can tell us a user encountered some errors. But, if the application uses djangonnewauth, the user provided by newauth will not be displayed.

I tried to write a SentryPlugin to display the newauth’s user information. As it turns out, I could not do it. It was a difficult than I thought.

Solution

I wrote a package to solve this raven_django_newauth .

You can set SENTRY_CLIENT = ‘raven_django_newauth.client.DjangoNewauthClient’, and then, User interface of Sentry provides a information of newauth’s user.

Sentry and Raven

Sentry collects informations sended by raven. Sentry defines general interfaces (for example a User), and raven traslates collected data to thing in consideration of these interfaces. A client send dictionary thats key is path to each interfaces (like ‘setry.interfaces.User).

The User interfaces in sentry.interfaces.User. And on django application, the raven client is raven.contrib.django.client.DjnagoClient. The client gets user information from get_user_info method and stores to the dictionary as a key ‘sentry.interfaces.User’

Djangoでアップロードしたファイルを非同期に削除する

Djangoでアップロードしたファイルを非同期に削除する

Django の FileField を通してアップロードしたファイルを非同期に削除したい。

本質的にはモデルが削除されると同時にファイルも削除したいというものなんだけど、 そこを同期処理しているとやってられないので、非同期で削除しようという話。

案1

  • Django のシグナルを使って、モデルが削除されたときに非同期でファイルを削除する タスクを呼び出す。

私はシグナルにいい思い出がなかったので、この方法はちょっと疎遠したかった。

案2

  • カスタムのストレージを書いて、ファイルの削除を非同期に行わせる。

これを採用。

残念な思い込み

FileField を持つモデルのインスタンスが delete されるときに、ファイルの delete も走ると思っていた。 でも実際はそうじゃなくてモデルが delete されても、ファイルの delete (ストレージの delete) は走らなかった(まぁ勝手に削除されるのはこわいか)。

明示的に FieldFile インスタンスの delete を呼ぶ必要があった:

class Test(models.Model):
    file = models.FileField(....)

test.file.delete()  # まぁこんなかんじ。

(ここでモデルのインスタンスにある file 属性は django.db.models.fields.FieldFile のインスタンス で、こいつの delete がストレージの delete を呼び出す)

書いたもの

非同期にファイルを削除するストレージを書いている。

でもまぁ結局 UploadedFile モデルの delete をオーバーライドして、ファイルも削除するようにしている。 これなら案1のように、 delete のシグナルでファイル削除用の非同期タスクを走らせても大差なかった。

非同期処理の書き方については自分のブログ記事が役に立った:

まとめ

残念

Djangoのチケット18481についての雑記

Djangoのチケット18481についての雑記

Django に投げていた パッチが昨夜取り込まれた のでその話。

前に取り込まれた チケット 18558 以来。

チケット 18481 について

このチケットは、別のチケット 17277 に由来してる。 17277 は POST の body 読み込み時にエラーがあれば、それ専用のエラーをあげるよう修正するというもの。

17277 以前は例外処理が入っていないので、例外があれば IOError が投げられていた。 でもこれってわかりにくいから、 IOError を継承した UnreadablePostError を投げるよう修正された。

でもこのチケット 17277 には漏れがあって、 FILES の場合はそのままだった。 なので今回取り込まれたチケット 18481 では、FILES で例外が発生するときも UnreadablePostError を 投げるよう修正しましょうというもの。

やったこと

私がこのチケットをみたときには、すでにバグ修正のチケットがあったのでテストのパッチを書いた (KyleMac に報告されて、 edevil が実装の修正を追加していた)。

owner もついていなかったしチケットも 2 ヶ月ほど放置されていたので、 ここはテストのパッチを書いてやるかと思った次第。

チケット 17277 で追加されたチケットを参考に、 FILES で例外が発生するようにしたテストを書いた。

取り込んでくれたのは claudep 、また彼にお世話になった。

PythonでWebサーバー書きましょう

PythonでWebサーバー書きましょう

「Webサーバー書いたことある?」

と聞かれ、何じゃらホイと尋ねると

「書いたことあれば分かりそうな問題にハマってる事例見てね。みんな書いたことあるわけじゃないのね、と思って」

とのこと。

そういえば私も書いたこと無かったのでWebの悟りを開くために書いた。

書いた

リクエストからURIとって、静的ファイルを読み込んでそのまま返す荒々しいヤツ

  • 404は返せません。
  • 500も返せません。
  • /etc/passwdは返せます。

socket-低レベルネットワークインターフェース 読んだだけ。

周りの人の話

  • とりあえずsocketで通信多重度1のstatic file返すだけのサーバー書くだけでもかなり勉強になると思うよ
  • waitress 読みやすくていいよ
  • GETだけなら簡単だよ
  • python以外で勉強として作りたいなら C でソケット開いて、まじめにパースする

との教えが。

まとめ

自己同一性の達成過程においてWebサーバー書くことは重要らしい。

ふとWSGIサーバーとか書きたくなる。

Pyramidでzope.interfaceを使う

Pyramidでzope.interfaceを使う

Pyramidzope.interface を使うときの話

zope.intreface

zope.intreface_ が面白いと最近知った。

実装を持たない「インターフェース」だけ定義して、 そこに後で実装をもたせる、というかんじ(超ざっくり)

折れ線グラフとしてのインターフェースをILinechart、 なにやら描画してくれるインターフェースをIRendererとして考えてみる。:

import zope.interface


class IRenderer(zope.interface.Interface):
    def render():
        """なんか描画する的"""


class ILinechart(zope.interface.Interface):
    series = zope.interface.Attribute("""Y軸の値みたいな""")
    category = zope.interface.Attribute("""X軸の値みたいな""")


@zope.interface.implementer(IRenderer)
class LinechartRenderer(object):

    __used_for__ = ILinechart

    def __init__(self, context):
        self.context = context

    def render(self):
        return str(zip(self.context.series, self.context.category))

こんなかんじか。 嬉しいのは:

  • インターフェースだけ先に書いといて実装はあとから持たせられる。
  • 継承みたいに書いた時点で関係が固定されるわけじゃない。

とかかな。正直面白半分で使い始めてる節あるので何とも。

zope.interface のドキュメント読んでこのへん読んどけば捗りそう:

Pyramidにおけるzope.interfaceの使い方

zope.interface とか zope.component でもアダプターの登録ができますが、 それは Pyramid でやりましょう。

Registry というものに登録する。 まぁconfigとかrequestにひっついてるから:

# config 経由で登録して
config.registry.registerAdapter(LinechartRenderer,
                                (ILinechart,), IRenderer, '')

# request 経由で取ると
adapted = request.registry.getAdapter(linechart, IRenderer, '')

# 良い感じ
adapted.render() # '[(0, 0), (1, 1), (2, 4)]' とか

でまぁ:

config.registry.registerAdapter(LinechartRendererJSON,
                                (ILinechart,), IRenderer, 'json')
config.registry.registerAdapter(LinechartRendererHTML,
                                (ILinechart,), IRenderer, 'html')

とかname変えて登録していくとさらに良い (この場合rendererのメソッドにもたせたほうがいいかもだけど)。

お作法

そんな感じで、アプリケーション書くときに

  • __init__.pyでconfig.registry.registerAdapter
  • view_callable内でrequest.registry.getAdapter

とかやっちゃうわけだけど、これはまぁ行儀良くないらしい。 zope.interface を意識しないといけないのはライブラリやフレームワークの開発者であって ユーザー(ライブラリ等を使って開発する人)には見せたくないとのこと[要出典]。

まあたしかに、もっとわかりやすい書き方してよって気になるしね。

そのお行儀の良い書き方というのは簡単で:

  • config.add_directiveでdirectiveを追加
  • そのdirective経由でアダプター登録
  • requestを受け取るAPIとしての関数から、アダプトされたオブジェクトを返す

というもの。 さらに venusian を使って、directive経由でアダプター登録してたのを デコレーターで書いてやることができる。

まぁこんなかんじになるのか(renderer_configとto_rendererは自分で書くのよ):

@renderer_config('',
                 chart_type='linechart')
def str_linechart(linechart):
    return str(zip(linechart.series, linechart.category))

renderer = to_renderer(linechart, '')
renderer.render()

このrenderer_configのなかではconfig.set_rendererとか呼び出して登録してやるといい。 さながらPyramidのview_configとconfig.add_viewみたいなもんである。

まあ一見に如かずなのでrebecca.todictを読めばいいと思う:

これを参考に私もpyramid_tochartというのを書いてるので、こっちも参考になるかも:

ただまあ良い書き方を模索してるところ

新サービスKarmaidをリリースしました

新サービスKarmaidをリリースしました

image

新規にWebサービス Karmaid をリリースしました。世界中すべてのものに未ログインで評価できるサービスです。 ここでは Karmaid というサービスについてと、リリース後にあった秒間200リクエストの話をします。

Karmaid とは

好きな文字列にインクリメント/デクリメントができるサービスです。 サイドバーの私のアイコンの下にあるのがそれです。

Karmaid の目的

Karmaid の目的は「気軽に何でも評価する」です。 特徴として以下の3点があります

  • プラス評価とマイナス評価の両方が提供されている
  • ログインが不要
  • 何にでも評価ができる

++と–の両方を提供

Karmaid ではプラス評価とマイナス評価を提供しています。 いいな、と思うときはインクリメント(++)を、そうでもないなと思うときはデクリメント(–)をします。

これで ただ単に拡散されているだけの情報を排除 できます。

image

ちなみに現時点での私のカルマは -16081 です。なるべく相手にしないほうがいいですね。

ログインが不要

これも Karmaid の大きな特徴です。 すべてのユーザーは制限された回数内で好きにインクリ、デクリができます。 ログインも不要なので、 Karmaid のボタンを設置しておけば誰でも気軽に評価することができます。

ログインが不要な場合、カルマの信頼性が下がりますが、ユーザーが十分いれば信頼できる値になるだろうと考えています。 不当な評価になりにくいよう作っていきます。

何にでも評価ができる

多くのWebサービスではURLを対象に人の評価を行う場合が多いです。ですが Karmaid にはその制限がありません。 人、もの、フレーズ、もちろんURLも含めてすべてにカルマが付けれます。

今後としては Non-ASCII のサポートや、URLに特化したKarmaidボタンを作って、より幅広く扱いやすくしていきたいです。

リリース後の反応

最大で222リクエスト/秒いきました。 8時20分くらいにリリースのアナウンスをしてから30分後くらいのことです:

08:52:50    222
08:52:55    217
08:52:46    216

分速では4000くらいでした:

08:52    4146
10:26    3893
09:55    2447

でもアプリは意外とピンピンしてました。アプリ以外のところがネックになったようです。 大量にデクリメントしてくれた方 (@kiris さん @ishikawa84g さん)、ありがとうございます。 よほどクリックがお得意なようです。

リリースアナウンスから今まで平均すれば、画面からポーリングしてる分含めて秒間1リクエストくらいですね。まぁそんなものかなと。

もちろん問題発見に一役買ってくれたり要望を投げてくれた @shomah4a さん kazyk さん inoshiro さん、ありがとうございます。 とりあえずalert文を入れてくれた方もありがとうございます。俺もよくやります。

おわりに

気に入ったら インクリしてください 。 あと Githubのリポジトリ でスターしてくれても元気がでます。

Karmaid をよろしくお願いします。

Djangoのテストで設定 (settings) を上書きする

Djangoのテストで設定 (settings) を上書きする

Djangoユニットテストを書くときに、 設定ファイル (settings.py) を一部変更したいことがある。

そのやり方をメモ。

テストの一部で変えたいとき

ドキュメントに書いてあった。

django.test.TestCase を継承したテストで with self.settings(HOGE=1) とする方法と django.test.utils.override_settings デコレーターを使って @override_settings(HOGE=1) と する方法があるよう。 override_settings デコレーターはクラスにもメソッドにもかけれる とのこと。

使いどころとしては、例えばキャッシュを使ってる場合に、寿命が切れてる場合の挙動を テストするとき。キャッシュが切れるまで待つのはいやなので、設定ファイルに 書いてあるキャッシュの寿命を0にしてやってテストする。 手動で消せるかつ、その方法で十分ならそれでもいいけど。

ちなみにこれは 1.4 からの機能。

1.3 以前で変えたい

setUpで設定ファイル一時保存して、tearDownで戻してあげる。

from django.conf import settings
from django.test import TestCase

class HogeTest(TestCase):
    def setUp(self):
        self.tmp_settings = settings
        settings.HOGE = 1

    def tearDown(self):
        settings = self.tmp_settings

これはちょっと悲しい。

こんなものもあった

これだと 1.2 - 1.4 で、上記のような @override_settings などが使えるらしい。 でも未検証なので気が向いたら試す。

全体で変えたいときは

他の方法としては、テスト用に設定ファイルを分けておくのも良い。 こんなかんじ:

settings
|-- __init__.py
|-- common.py
|-- dev.py
|-- prod.py
`-- test.py

テスト時のDB設定やログファイルの出力先を変えるときなどはこうする。 大抵の場合は開発用の設定ファイルと同じで十分。

開発用の設定ファイルがリポジトリ管理下に無くて、プロジェクトメンバーが 自由に設定できる場合などは、テスト用の設定ファイルがあるといいかもしれない。

でもまぁ結局「このテストのときだけ値を変えたい」という場合には 対応できない。

Django 1.4 以降を使っているなら override_settings を使うのがよさそう。 ただ 「 settings.test で値を変更したのにテストに影響がない?!」 とか言って override_settings 使ってました、とかはありそう。

使うのを最小限にするのと、設定ファイルはテスト内で上書きされ得ると頭の片隅に置いとくと いいかも。

参考: