Make組ブログ

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

Search view with long long parameters

Search view with long long parameters

At most of web application have the so-called search view. The view get some request parameters and display some search results by using these parameters.

A search view does not cause any side effects, so it should expect a GET request. The URL will be like this:

/search?code=R100100&code=R1001001

and the view will be like this:

def search_studends_view(request):
    f = SearchForm(data=request.GET)

    students = None
    if f.is_valid():
        codes = f.cleaned_data['code']
        students = get_students(codes=codes)

    return HttpResponse(",".join(students))

(This is a sample code, so I have not verified the correctness of it)

In most cases, the GET search view seems good and enough. But unfortunately, it have some limitations.

With long long parameter

Let’s consider a searching by using very long parameters. If you should create a view that can be OR search by 1,000 birthdays…

The URL will be like this:

/search?code=R100100&code=R100101&code=R100102&code=R102000&code=...

Then, what would happen? (You may have a doubt the requirement itself)

The header of this request will be very long too. so, It will couse “Request URI too large (414)”.

Gunicorn’s maxcimum size of a request line is 4094 bytes by default.

At Nginx, it is 8k bytes.

If you use reverse proxying, you also should consider settings proxy_buffers, proxy_buffer_size.

だるい。

With POST

Another solution, you can use POST request with search view.

But, if you make the honest, you will run into trouble caused browser history backing. If you back to POST result, most browser will raise warning (like ‘Web page has expired’).

A re-POSTing is handled as bad process to protect such as multiple registration. Also, the POST results will not be cached, because a POST request cause side effect in general. (Some browsers like Chrome, Firefox will cache it. but IE will not so even if you provide Cache-Controll header)

To avoid this, you should redirect to a GET result page after POSTed. POSTed parameter will store to session, and GET page display by using it.

like this:

def search_studends_view(request):
    if request.method == 'POST':
        request.session['search_params'] = request.POST
        return HttpResponseRedirect('/search')
    else:
        try:
            params = request.session['search_params']
        except KeyError:
            params = {}
        f = SearchForm(data=params)

        students = None
        if f.is_valid():
            codes = f.cleaned_data['code']
            students = get_students(codes=codes)

        return HttpResponse(",".join(students))

(Yes, this is a just sample code)

It is not beautiful.

If you want to paginate these results you shuould get a page number from GET parameter (puke).

And more

Serializing and compressing GET parameters on client side may also be good solution.

Don't format strings before logging in python

Don’t format strings before logging in python

You should not provide formatted string to loggers in Python, like this:

logger.info('Logged in: %s' % username)

You should write like this:

logger.info('Logged in: %s', username)

Why?

In mont cases, it is the same as a result. But, internally, the later one contains more information which part of this string represents a username.

You shoud realize the first argument (message) can also be used as a signature.

If you want to aggregate logs, you will group logs by messages, like this:

  • message: ‘Logged in: %s’, args: (‘Ritsu Tainaka’,)
  • message: ‘Logged in: %s’, args: (‘Mio Akiyama’,)

Yes, you will be able to group these logs. They are same log, just username is different.

OK, now consider this grouping with formatted strings, it will be not work:

  • message: ‘Logged in: Ritsu Tainaka’, args: ()
  • message: ‘Logged in: Mio Akiyama’, args: ()

They will be handled as different. Of cause, these messages are totally different.

Practical

Sentry, error logging and aggregation platform, it displays logs grouping by these messages.

So, If you use Sentry, you should provide not formatted message to any loggers. Without this, all logs containing some variables will be handled as different. Yes, as thousands of different logs.

Django's TestCase.multi\_db attribute is mistake

Django’s TestCase.multi_db attribute is mistake

Django’s test framework (django.test.TestCase) has a atribute multi_db . It should be set True when testing on multiple databases (False, by default).

If you forget this setting, and your test uses multiple databases, the test suite only flushes the ‘default’ database (more exact django.db.utils.DEFAULT_DB_ALIAS) without flushing another databases.

And then, some trash datas will left on these databases (ignore the ‘default’ database). After tests will have risk of failing in absurd reason. I’m handling multiple databases on my work, and every time I forget setting this. And the mistake is difficlut to notice. Of cause, there are no errors. This is expected behavior.

This behavior (only flushing ‘default’) is feature for speeding up. Because, flushing a database is slow, and it will be run for each tests.

But, I think, it should be flush all databases by default.

If your application using multiple databases, but a TestCase use only ‘default’, then you can set ‘flush_only_default = True’ (for example) to force a test suite flushing only ‘default’ database.

Of cause, your application uses only ‘default’ database, only ‘default’ will be flushed even if without setting flush_only_default = True.

I think the behabior for speeding up is optional. By default, it should be performed as indubitable even if it will be slow.

The change for this will be like this:

diff --git a/django/test/testcases.py b/django/test/testcases.py
index a9fcc2b..0558476 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -466,11 +466,11 @@ class TransactionTestCase(SimpleTestCase):
     def _databases_names(self, include_mirrors=True):
         # If the test case has a multi_db=True flag, act on all databases,
         # including mirrors or not. Otherwise, just on the default DB.
-        if getattr(self, 'multi_db', False):
+        if getattr(self, 'flush_only_default', False):
+            return [DEFAULT_DB_ALIAS]
+        else:
             return [alias for alias in connections
                     if include_mirrors or not connections[alias].settings_dict[
-        else:
-            return [DEFAULT_DB_ALIAS]

     def _reset_sequences(self, db_name):
         conn = connections[db_name]

I asked about this proposal on django developers IRC channel (#django-dev on freenode). Some people answered me (Thanks a lot!), in side disagree. Certainly, this proposal is not have great gain. and having a big risk breaking compabitity. I noticed this is not good proposal, but I still claim the multi_db behavior is mistake.

Yes, I will set multi_db = True on a base class, and subclassing it on own tests.

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サーバーとか書きたくなる。