2011年12月23日金曜日

twistedでのunittest

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

twistedでunittestをするときは、twisted.trialモジュールを利用する。 基本的にはbuilt-inのunittestと同じなのだが、deferredが存在するための仕組みが備わっている。 ここ にドキュメントがあるのだが若干分かりにくい。というか、結論を先に書いてくれ!

3つ大事なコトがある。

  1. deferredを返す関数をテストするtest methodはdeferredを返せ。
  2. timeoutをつかえ
  3. reactor.run()、reactor.stop()、reactor.crash()、reactor.iterateを呼ぶな

1)の原文は

The golden rule is: If your tests call a function which returns a Deferred, your test should return a Deferred.

である。callbackを勝手によんでくれます。もしかしたらcallLaterを使わないで値を渡す賢いやり方があるのかもしれません。

2)は、timeoutを設定することになる。これにはメソッドのアトリビュートを使用する。 これは下記のサンプルコードをみれば了解できると思われる。というか、ドキュメントは3行も書いているが、 なんでサンプルコードを載せないのか理解に苦しむ。

ここに書いたサンプルのコードを実行すると、2つのテストが成功し、最後の物がtimeoutする。

3)は守れ。理由はreactorのインスタンスの数を考えれば自明(trial自身がreactorを持っている)

#!/usr/bin/python
# -*- coding=utf8 -*-


'''
  http://twistedmatrix.com/documents/current/core/howto/testing.html

  how to run:
$ trial twisted

'''
from twisted.internet import defer, reactor

from twisted.trial.unittest import TestCase
# c.f. from unittest import TestCase



class MyTestCase(TestCase):
  def test_with_deferred_ok(self):
    d = defer.Deferred()

    def fire(ignore):
      return 'deadbeaf'

    d.addCallback(fire)
    def ok(s):
      self.assertEquals(s, 'deadbeaf')
    d.addCallback(ok)
    def fail(s):
      self.assertNotEquals(s, 'deadbeaf')
    d.addErrback(fail)

    #reactor.callLater(0, d.callback, 'deadbeaf')
    return d
    '''
      The golden rule is: If your tests call a function which returns a Deferred, your test should return a Deferred.
    '''

  test_with_deferred_ok.timeout = 1
  '''
    The way to do this in Trial is to set the .timeout attribute on your unit test method. 
    Set the attribute to the number of seconds you wish to elapse before the test raises a 
    timeout error. Trial has a default timeout which will be applied even if the timeout 
    attribute is not set. The Trial default timeout is usually sufficient and 
    should be overridden only in unusual cases.
  '''
  def test_with_deferred_fail(self):
    d = defer.Deferred()

    def fire(ignore):
      return 'moomoo'
    d.addErrback(fire)

    def ok(s):
      self.assert_(False)
    d.addCallback(ok)
    def fail(s):
      self.assertNotEquals(s, 'deadbeaf')
    d.addErrback(fail)

    return d

  test_with_deferred_fail.timeout = 1

  def test_with_deferred_timeout(self):
    d = defer.Deferred()

    def ok(s):
      self.assert_(False)
    d.addCallback(ok)
    def fail(s):
      self.assertNotEquals(s, 'deadbeaf')
    d.addErrback(fail)

    reactor.callLater(2, d.errback, 'moomoo')
    return d

  test_with_deferred_timeout.timeout = 1

2011年11月29日火曜日

パズル:おいらの回答

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
one offやりまくってしまった。orz ideaとしてはheadとtailをとって短くしていく、tailとheadを入れ替える、ということ。再起が分からない人にはかけないやつね。
#include 
#include 
#include 


void reverse (char *str) {
  char head, tail;
  int len;


  len = strlen(str);
  if (len == 0){
    return;
  }
  if (len == 1){
    return;
  }

  tail = *(str+len -1);
  *(str+len -1) = '\0';
  head = *str;
  reverse(str+1);
  *(str+len -1) = head;
  *str = tail;
  return;
}

int main (int argc, char *argv[]) {
  if (argc *lt;= 1) {
    printf("give me a string\n");
    exit(1);
  }
  char *str = argv[1];

  reverse(str);
  printf("%s\n", str);
  return 0;
}
思ったこと:

strlenを使ってよい(というか仕方ないだろう)はどーなのでしょうか。中身loopだからこれ実行したらO(n^2)だよ、多分。

まあ、パズルだから許されるだろうけど、職場で同僚がこういうコードを書いていたらヤバい。なぜなら:

from ゲリラ的雇用面接のすすめ 関数の実行速度は充分速いか? strlen を何回呼ぶ事になるのか考えてみよう。 かつて私はO(n)であるべきstrrevのアルゴリズムがO(n^2)になってしまっている アルゴリズムを目にした事がある。彼らは繰り返しの strlen をループの中で呼ん でしまっていたのだ。 まあ、問題を
void reverse (char *start, char *end) {
    return;
}

とか書き換えればいいのですが(C++のイテレータチックだな)そうすると問題が変わってしまう。

やる気に関する驚きの科学 (TED Talks)に出てくる、この絵の問題のようなものだ。

オリジナルCパズルの問題は前者、書き換えたものはfor dummiesになっているのではないかと思う。

C言語パズル?

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
reverseを下請け函数なし、loopなし、memmove/memcopyなしで実装せよ
#include 
#include 
#include 


void reverse (char *str) {

  /* place your code here. */

  return;
}

int main (int argc, char *argv[]) {
  if (argc <= 1) {
    printf("give me a string\n");
    exit(1);
  }
  char *str = argv[1];

  reverse(str);
  printf("%s\n", str);
  return 0;
}

2011年11月23日水曜日

tumblr関連のスクリプティングまとめ・反省

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
唐突にtumblr関係のプログラムをごにょごにょしたので、その簡単なまとめと反省。

コードはgithubに置いてある。

メモを見ると
Goal
非同期書き込み非同期読み込み、重複contentなしのtumblr client
pointされている画像とかも表示できる(twitterのpicsとか)
らしい。


  • 最初の頃はUIを作る気でいた。面倒なのでpythonを使うつもりでいた。
  • getをかけている間固まるのが厭だったので、twistedを選択。(まあまあの決断かな。遠い未来は分からないので悪い決断出なければよしとすべきだろう。)
  • pyqtをインストールできないのでwxを試す。
  • とっつきはwx.html.windowを使ってレンダーしたのだったが、依存する画像データのメモリ渡しが出来ない様だったので捨てる。このままでは表示系がないのでproxyとして実装して、UIは後回し。(これはよい決断だったと思う。)
  • とりあえず動かしたproxyでもfirefoxの読み込みの遅さが改善されて驚く。どういう作りなんだか。IPv6がらみ??(根拠なし)
  • proxy上で特定のhostに関してhookするやり口はよかったのだが、hostにマッチする方法がやりすぎだった。
  • proxyとcacheのプロセスを分けたのは正解だった。pluginも分離すべきかもしれない。repが短くなりデバッグがやりやすい。

  • dashboardの取得でハマる。ブラウザのクッキーを覗き見して使うのではなくてapiを使うべきだった。(反省)

  • twisted.web.client.getPageとかでのヘッダーの扱いで思い込みが多かった。(反省)
    • uaやcookieを渡しても書いてくれるだろう→ダメ。
    • content-typeを付けてくれない! c.f. urllib.openurlは勝手にpostにしてくれるし、コンテントタイプを付けてくれる。
今後
  • tumblr api v2はoauthなのだが対応するのか? メリットデメリットは?
  • package cacheデータの置き場所とか
  • prefetchの機能改善:
    • 現状、/api/dashboardを読み込んだときのxmlのデータでpointされる画像しか拾っていない
    • dashboardのhtmlをlocalで生成する。(/dashboard読み込みの遅さを改善する。)
    • endless scrollに対応する。
    • cacheされていなかったことでmissしたcontentもcacheに書き込む。
  • reblog graphの生成とか重複排除とか・・・たぶんpostのdbが必要になる。
  • UIどうしよう?外部プレーヤの起動もやってみたい。

2011年11月8日火曜日

先日のLVM関係の騒ぎのときに拾ったpdfより

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

まあ、デバッグとかと同じで、想定している値と食い違った値を出している箇所を特定する作業が必要なのですが、
レイヤーごとにどうなっているか、下からチェックしなさい、といっている。


出典:LVM2 – data recoveryという、Milan Brož(mbroz@redhat.com)さんがが書いたプレゼン。

2011年11月2日水曜日

VMware Player 上のguest OSに生LVM Volumeをマウントする

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
HostはScientific Linux 6.1です。VMWare Playerは4系列。

まあ、ここにある結論をそのまま使っただけですけどね。


結論:ディスクイメージの元のHDDのジオメトリが分からなければ、総セクタ数としてファイルサイズ÷512(の小数点以下を切り捨てた整数)を総セクタ数として記述しておき、セクタ数、ヘッド数、シリンダ数は“0”としておけばいい。その状態で仮想マシンを立ち上げると、適当なセクタ数、ヘッド数、シリンダ数をVMwareの仮想ディスクドライバが決定してくれる。もし、定義ファイルに“0”のままでは気になるのであれば、hdparmで得たドライバが決定したジオメトリの値を定義ファイルに書き込んでおけば良い。というところだろうか。

今時のコンピュータでは、ディスク上の情報の位置は先頭からのセクタ番号だけアクセスして、昔の様にヘッド番号とかシリンダ番号、トラック内のセクタ番号を使ってアクセスしないので、これでも問題ない、ということかもしれない。(fdiksがHDDの物理ジオメトリは無視して、セクタ数を63、ヘッド数を255にしてパーティションするのも、この考え方であれば納得できる。)


実作業としては:

1. なんか適当にtemplateとなるvmdkファイルを作る。metadataの中身はこんな感じ。容量指定はなんでもいいが、preallocateしたほうがよいだろう。
# Disk DescriptorFile
version=1
encoding="UTF-8"
CID=8d03ddbb
parentCID=ffffffff
isNativeSnapshot="no"
createType="twoGbMaxExtentFlat"

# Extent description
RW 208896 FLAT "Win7Template-f001.vmdk" 0

# The Disk Data Base 
#DDB

ddb.adapterType = "lsilogic"
ddb.geometry.sectors = "32"
ddb.geometry.heads = "64"
ddb.geometry.cylinders = "102"
ddb.uuid = "60 00 C2 9f 1a d0 8e 7c-04 3a 03 fe fe 0f 15 52"
ddb.longContentID = "b0e2674d8fa7d5101c4addeb8d03ddbb"
ddb.virtualHWVersion = "8"

2. 上記のファイルをLVMのLVの位置・容量に合わせて書き直す。
# Disk DescriptorFile
version=1
encoding="UTF-8"
CID=51be770e
parentCID=ffffffff
isNativeSnapshot="no"
createType="twoGbMaxExtentFlat"

# Extent description
RW 629145600 FLAT "/dev/vg_akagi/ntfs" 0

# The Disk Data Base 
#DDB

ddb.virtualHWVersion = "8"
ddb.longContentID = "e46ca8b5dbfce4905651da8a51be770e"
ddb.uuid = "60 00 C2 9f 1a d0 8e 7c-04 3a 03 fe fe 0f 15 52"
ddb.geometry.cylinders = "0"
ddb.geometry.heads = "0"
ddb.geometry.sectors = "0"
ddb.adapterType = "lsilogic"

3. VMware Playerを実行するUserをdisk groupに追加。さすがにrootにするのは萎える。これをやっておかないと、/dev/vg_akagi (実態は/dev/dm-XXX)などにはアクセスできない。


4. guestの設定を編集し、既存のharddisk imageとして追加する。

5. Playerを起動してguestを動かす。
注意: guestがマウントしている間はhost上でマウントしないこと!!!ぶっ壊れるよ!1つのdiskに対してファイルシステムが起動することになるから。なので間違ってもfstabに登録するとかバカなことはしないように。

2011年10月31日月曜日

Scientific Linux 6.1 x86_64 setupメモ

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
1.RedHat系で暮らすとしたら、やはりyumのrepositoryの設定が超重要。

baseurlを書き換えるアホな設定が人口に膾炙しているが、mirrorlistをファイルで用意するという方法が正しい(と思う)。
具体的には:

Scientific Linux 6.1 での yum リポジトリ設定
を参照されたし。fastbugsはぜひ当てておきたい。

2. video
video card関係の設定はproprietaryなdriverを入れることになるのだが、ATIはXが立ち上がった状態でインストールできる。ただし、gccとかカーネルヘッダが入っていないとdistribution specificなものを生成するを選んだ場合、コンパイルされないので、注意が必要。xorg.confを触ってはいけない。amdcccleで設定する。再起動が何回か必要になる。xineramaは一番最後に有効にすること。(すべてのvideo cardを有効にする→ローテーションを修正する→xinerama)

3. browser
RHについているFireFoxは古いので新しいのをDLするが、普通にDLするとi686で動かない。snapshotだかなんだかのページに行ってx86_64を選んで落としてくる。

4. VMware
開発workstationのインストールを選択すればqemuが入っているが、好みで入れる。メールで送られてくるDL pageのリンクが間違っているので、これもリンク先のファイル名をみて判断する。(x86_64という文字列が含まれるものを落とす)

5. 設定につかったrpmとかはbackupする

6. /をlogical volumeの上に置かない。(壊れたときに復旧が面倒)

7. partitionを大きくしすぎない。file imageとしてbackupがとりにくい。

8. BIOSの設定には要注意。私のMBではUEFI bootをサポートするとUSB Keyboardが効かなくなった。UEFIがないと2TiB以上のHDDでは起動できない。

9. MotherBoardの機能を利用したRAID1は、インストール完了後にdiskをシンクロさせようとする。その間はリセットボタンは避けた方がいいだろう。なので、video cardのconfigとかでハマりそうだったら、シンクロが終わるまで待つこと。

2011年10月28日金曜日

LVM+MB RAID(続き)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
pvckをつかえばよかったのだ。
root@livecd fromAkagi]# pvck -v /dev/sdb2
    Scanning /dev/sdb2
  Found label on /dev/sdb2, sector 1, type=LVM2 001
  Found text metadata area: offset=4096, size=1044480
    Found LVM2 metadata record at offset=8192, size=1536, offset2=0 size2=0
    Found LVM2 metadata record at offset=6656, size=1536, offset2=0 size2=0
    Found LVM2 metadata record at offset=5632, size=1024, offset2=0 size2=0

この情報と前回の投稿で得られているLVの情報を合わせると:
  • metadataが置かれているoffset 4KiB
  • metadata自体のSize1020KiB
  • データ(LgVolHome)のoffset 71680000KiB
  • データ(LgVolHome)の長さcount 102400000、(1KiBの個数)
なので・・・
 dd bs=1K skip=71681024 count=102400000 if=/dev/sdb2 of=home2.img
とかすればいい。ofの先頭だけでも書き出されて入れば、
[root@livecd fromAkagi]# file home2.img 
home2.img: Linux rev 1.0 ext4 filesystem data (extents) (large files) (huge files)
と判定されて、正しいと思われる部分を切り出したと言えそう。ddが終わり次第、-o loopでmountして中身をチェック、
大丈夫そうなら、壊れていると思われるLgVolRootをddして修復し、書き戻せばいい。

2011年10月27日木曜日

LVM+MB RAID

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
MotherBoardの機能を利用したRAID1を組んだのだが、どういうわけだか/dev/sdaと /dev/sdbがシンクロしないらしく、LVMのlogical volumeが読み書き不能になった。LVじゃないpartitionは大丈夫なのだが。いまどきdiskは安いのででっかくアロケートしてLVMでちまちま切り出して使うことは止めましょうかねぇ。

ともあれ、LVM関係のDocumentを読み漁ることになって、いままで概念的にしか知っていなかったLVMの実装っぽいところまで見るハメに。概念的な話なら、@ITとかにいい記事があったと思うし、結局「もう一段レイヤーをはさむ」ありがちな話なので難解ではない。

で、「ソフトウェアを分かるにはデータ構造を見よ」というありがたい教えにしたがい、メタデータの構造を調べた。Red HatのLVM metadataに関するドキュメント

「物理ボリュームラベルは文字列 LABELONE で始まります。」ということなので、ddした結果をhexdumpしgrepした。
hexdumpは不要かもしれないが、それではまわりがどうなっているのか分からんので、富豪的なアプローチをとった。

[root@livecd fromAkagi]# dd bs=512 skip=1000K count=128K if=/dev/sda | hexdump -C | grep LABEL
00100200  4c 41 42 45 4c 4f 4e 45  01 00 00 00 00 00 00 00  |LABELONE........|
00494110  3a 49 44 5f 46 53 5f 4c  41 42 45 4c 3d 6e 74 66  |:ID_FS_LABEL=ntf|
004e1120  3a 49 44 5f 46 53 5f 4c  41 42 45 4c 3d 74 65 73  |:ID_FS_LABEL=tes|
004e1130  74 0a 45 3a 49 44 5f 46  53 5f 4c 41 42 45 4c 5f  |t.E:ID_FS_LABEL_|

ddして周辺のデータを生きているfs上のfileとして持ってくる。
[root@livecd fromAkagi]# dd bs=512 skip=1001K count=128K if=/dev/sdb of=head-sdb.img
131072+0 records in
131072+0 records out

[root@livecd fromAkagi]# dd bs=512 skip=1001K count=128K if=/dev/sda of=head-sda.img
131072+0 records in
131072+0 records out

ここからの引用になりますが、

ボリュームグループ情報が含むものとしては
  • 名前と独自の識別子
  • メタデータが更新される度に上昇するバージョン番号
  • プロパティ: 読み込み/書き込み? サイズ変更可能?
  • 含まれる物理ボリュームと論理ボリュームの数量に対する管理制限
  • エクステントのサイズ(512 byte として定義されるセクターのユニットで表示)
  • ボリュームグループを構成する物理ボリュームの自由配列一覧、それぞれ以下を含む:
    • UUID:それを含有するブロックデバイスの決定に使用
    • プロパティ:物理ボリュームの割り当て可能性など
    • 物理ボリューム内の一番目のエクステントの開始点までのオフセット(セクターで表示)
    • エクステントの数量
  • 論理ボリュームの自由配列一覧、それぞれ以下を含む
    • 論理ボリュームセグメントの順番配列一覧。それぞれのセグメント用にメタデータは
      物理ボリュームセグメント、または論理ボリュームセグメントの順番配列一覧に適用 するマッピングを含んでいます。
とからしい。
実際、dumpを順番に見ていくと、こんな感じで整理できる情報が手に入る。かなり編集してあります。
PVをどっからとっているのかとか、どのように使っているのか、とかねぇ。LVを追加すると、
どんどん後ろにappendされていくようだ。たしかに、flushのタイミングがおかしくなったときに
情報が失われてしまうからね。
812000... => PV info? seq=1, empty
816000... => PV info? seq=2 on LogVolSwap
  LogVolSwap 0, 17500, on [PV0, 0]
81a000... => PV info? seq=3 on LogVolSwap, LogVolHome
  LogVolSwap 0, 17500, on [PV0, 0]
  LogVolHome 0, 25000, on [PV0,17500]

820000... => PV info? seq=4 on LogVolSwap, LogVolHome
  pv0 = /dev/md127p2
  LogVolSwap 0, 17500, on [PV0, 0]
  LogVolHome 0, 25000, on [PV0,17500]
  LogVolRoot 0, 25000, on [PV0,42500]

826000... => PV info? seq=4 on LogVolSwap, LogVolHome
  pv0 = /dev/md127p2
  LogVolSwap 0, 17500, on [PV0, 0]
  LogVolHome 0, 25000, on [PV0,17500]
  LogVolRoot 0, 25000, on [PV0,42500]
  LogVolRoot 0, 159306, on [PV0,67500]
いっぽうで、PV/PEに関する情報は
PE info
extent size=8192 (x512bytes, 4MiB)
pe_start = 2048
pe_count = 226806
なので、総合すると、すべてのPEとLEを使っていて、226806で一致している、PV0を線形に使っているので、
offsetを計算してあげれば、ddをつかってimageを作れるはず、というとことまできた。

が、offsetの計算がなんか怪しいらしく、取り出したimageのfile system ext4が壊れているかもしくは
存在しないことになってしまっている。LVMのmetadataを利用してdeviceを作ることができればもうちょっと作業が
楽になるハズが・・・。

がー、なんか手動で--maps相当をやっていたようだ。 via これ

LVM を利用していると、ディスク不良が発生した時に不良箇所がファイルシステム上のどこに相当するかを調べるのが面倒になります。具体的にはコマンド pvdisplay のオプション --maps で割り当て状況を確認し、物理ボリューム上のどの位置がどの論理ボリュームのどこに相当するのか確認しなければなりません。このため実運用では RAID 構成のストレージ上に LVM パーティションを作成する事をお勧めします。


こんなような結果が得られる。
  --- Physical volume ---
  PV Name               /dev/sdb2
  VG Name               vg_akagi
  PV Size               885.96 GiB / not usable 0   
  Allocatable           yes (but full)
  PE Size               4.00 MiB
  Total PE              226806
  Free PE               0
  Allocated PE          226806
  PV UUID               yIkDlv-9zdI-wj9A-YaGd-pKc3-8uNH-sOZhTa
   
  --- Physical Segments ---
  Physical extent 0 to 17499:
    Logical volume /dev/vg_akagi/LogVolSwap
    Logical extents 0 to 17499
  Physical extent 17500 to 42499:
    Logical volume /dev/vg_akagi/LogVolHome
    Logical extents 0 to 24999
  Physical extent 42500 to 67499:
    Logical volume /dev/vg_akagi/LogVolRoot
    Logical extents 0 to 24999
  Physical extent 67500 to 226805:
    Logical volume /dev/vg_akagi/LogVolData
    Logical extents 0 to 159305
先日書いたpython scriptでsdaとsdbを比較した結果(sda2とsdb2じゃないのがイケてない。sda1/sdb1には500Mくらいの/bootのpartitionが存在する)では174815245KiBあたりから不一致だった。PE/LE換算42554あたりから42992あたりまでおかしいので、Rootの部分がなにやらおかしなことになっていると検討がつく。Kernel Panicがおこることが説明できる。

今後の作業は
  • 無傷であると思われるDataとHomeの救済、backup

  • Rootの修復

である。sda/sdbの間でRootの部分を上書きコピーしてやる荒技もアルが、そのときはsda/sdbで食い違う部分をddして保管したのちに試みるべきだね。

2011年9月22日木曜日

deployを怪しく自動化

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

pip の-rで渡すファイルの中でsourceの取得を使うことができるのだが、それをもっと賢く使えば良
かったかも。sh scriptがかけない人なので、なんでもpythonで書いちゃう、すべてが釘に見える人。しかも無駄(?)にきれい。本人は脳みそのキャパが小さいのでこのくらいきれいじゃないと死んじゃうんです。


いくつかポイント

  • activateのコードを読んだ上で必要だと思われた、環境変数を待避してます。(でもスタックを作るのはやり過ぎだったかも。sub-Targetでなんかbuildするなら必要かもしれない。)
  • subprocessを乱用してソースコードをかき集め、そこに含まれているfreeze.txtをみてpip installを行い、依存関係を解決する。

  • build_procで指定した関数を実行して必要なものを生成。(rstからsphinxをつかってhtmlを生成)


まあ、気の利いたツールだとソースを取ってくるところとか非同期並列なんでしょうけど。あと依存関係と処理済みかどうかを判定して必要な部分だけを作業するとかね。今回の目的は「マニュアルに書くとはまる」ことを回避することにあるので、そういうのは別にどうでもいいのです。いちいちvirtualenvを切り替えたくなかったし。

virtualenvも環境を継承できればいいのになぁ、と思う。よくドキュメントを読んでいないせいかもしれない。ていうか、こういうどのpackageを使うかを管理する機能をpython自体に持たせてほしい。何回もgetされるとだるいので、cacheしてほしいし .i.e packageの実体とpackageがインストールされている事実を分離して(eggのdevとかできてるよな)、インストールされている事実だけをenvごとにもってほしいよね。

import os
import os.path
import subprocess
    
class VirtualEnv(object):
  '''
    VIRTUAL_ENV=/home/ubuntu/virtualenv/ci-proj
    PATH=/home/ubuntu/virtualenv/ci-proj/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
    PS1=(ci-proj)\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\u@\h:\w\$ 
  '''
  _names = ('VIRTUAL_ENV', 'PATH', 'PS1',)

  def __init__(self, **kws):
    for n in self._names:
      if n in kws:
        self.__dict__.update({n: kws[n]})

  def read(self):
    for n in self._names:
      self.__dict__.update({n: os.getenv(n)})

  def __eq__(self, other):
    for n in self._names:
      if getattr(self, n) != getattr(other, n):
        return False
    return True
  
  def activate(self):
    '''
      $ source ~/virtualenv/deploy/bin/activate
    '''
    for n in self._names:
      os.environ[n]=getattr(self, n)
    subprocess.call(['env'])
 
  def deativate(self):
    '''
      $ deactivate
    '''
    # activate some thing else.

  def __str__(self):
    return ''%(id(self), ' '.join(['%s:%s'%(n, getattr(self, n)) for n in self._names]))

class EnvStack(object):
  def __init__(self):
    self._stack = []
    v = self.get_current()
    self._push(v)
    self.check_invariant()

  def check_invariant(self):
    v = self.get_current()
    u = self.top()
    if v != u:
      print v
      print u
      print self._stack
    assert v==u

  def close(self):
    while self._stack > 1:
      self.pop()

  def get_current(self):
    v = VirtualEnv()
    v.read()
    return v

  def top(self):
    return self._stack[-1]

  def push(self, v):
    self.check_invariant()
    v.activate()
    self._push(v)
    self.check_invariant()

  def _push(self, v):
    self._stack.append(v)

  def pop(self):
    self.check_invariant()
    self._stack.pop(-1)
    v = self.top()
    v.activate()
    self.check_invariant()

class Target(object):
  def __init__(self, src, branch, directory, dep=None, entrypoint=None, build_proc=None):
    self.src = src
    self.branch = branch
    self.directory = directory
    self.entrypoint = entrypoint
    self.build_proc = build_proc
    if dep:
      self.dep = dep
    else:
      self.dep = tuple()

  def mypath(self, builddir):
    return os.path.join(builddir, self.directory)

  def start_context(self, builddir):
    self._cwd = os.getcwd()
    os.chdir(self.mypath(builddir))

  def end_context(self):
    os.chdir(self._cwd)

  def get(self, builddir):
    subprocess.call(['git', 'clone', '-n', self.src, self.mypath(builddir)])
    self.start_context(builddir)
    subprocess.call(['git', 'checkout', self.branch])
    self.end_context()

  def pip(self, builddir):
    self.start_context(builddir)
    subprocess.call(['pip', 'install', '-r', 'freeze.txt'])
    self.end_context()

  def resolve(self, builddir):
    for d in self.dep:
      d.prepare(builddir)
      t = os.path.join(os.getcwd(), builddir, d.directory, d.directory) #FIXME
      self.symlink(builddir, t)
    self.pip(builddir)
    
    
  def build(self, builddir):
    if self.build_proc:
      self.start_context(builddir)
      self.build_proc(self)
      self.end_context()

  def prepare(self, builddir):
    self.get(builddir)
    self.resolve(builddir)
    self.build(builddir)

  def symlink(self, builddir, target):
    self.start_context(builddir)
    subprocess.call(['ln', '-s', target])
    self.end_context()

  def run(self, builddir):
    self.start_context(builddir)
    subprocess.call(['python', self.entrypoint,])
    self.end_context()

tonic = Target(
          src='',
          branch='migrate',
          directory='tonic')
bglib = Target(
          src='ひみつ',
          branch='migration',
          directory='bglib')
        
def build_doc(t):
  os.chdir('doc')
  subprocess.call(['make', 'html'])

imageserver = Target(
          src='ひみつ',
          branch='master',
          directory='imageserver',
          dep=(tonic, bglib),
          entrypoint='sample.py',
          build_proc=build_doc)

env = VirtualEnv(
  VIRTUAL_ENV='/home/ubuntu/virtualenv/deploy',
  PATH='/home/ubuntu/virtualenv/deploy/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games',
  PS1='(deploy)\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\u@\h:\w\$',
)
#print env

s = EnvStack()

def deploy(builddir):
  s.push(env)
  os.mkdir(builddir)
  imageserver.prepare(builddir)
  s.pop()

def run(builddir):
  s.push(env)
  imageserver.run(builddir)
  s.pop()
  
def clean(builddir):
  subprocess.call(['rm', '-rf', builddir])
  
if __name__ == '__main__':
  import sys
  cmd = sys.argv[1]
  print repr(cmd)
  builddir = 'build'
  if cmd=='run':
    run(builddir)
  elif cmd=='deploy':
    deploy(builddir)
  elif cmd=='clean':
    clean(builddir)
  else:
    print 'unknown cmd', cmd

2011年7月11日月曜日

写経みたいなもの

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

練習問題への解答

ローマン数字を整数に変換する。peekの実装で、''が読み出されたときにrewindしてしまっていた。


tes.py
import unittest
from roman import roman2numeric

# cases made from http://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%BC%E3%83%9E%E6%95%B0%E5%AD%97

class TestRome2NumericBasicCase(unittest.TestCase):
  def test_none(self):
    self.assertEqual(roman2numeric(''), 0)
  def test_one(self):
    self.assertEqual(roman2numeric('I'), 1)
  def test_five(self):
    self.assertEqual(roman2numeric('V'), 5)
  def test_ten(self):
    self.assertEqual(roman2numeric('X'), 10)
  def test_fifty(self):
    self.assertEqual(roman2numeric('L'), 50)
  def test_hundred(self):
    self.assertEqual(roman2numeric('C'), 100)
  def test_fivehundred(self):
    self.assertEqual(roman2numeric('D'), 500)
  def test_thousand(self):
    self.assertEqual(roman2numeric('M'), 1000)


class TestRome2NumericCombinedCase(unittest.TestCase):
  def test_two(self):
    self.assertEqual(roman2numeric('II'), 2)
  def test_three(self):
    self.assertEqual(roman2numeric('III'), 3)
  def test_six(self):
    self.assertEqual(roman2numeric('VI'), 6)
  def test_seven(self):
    self.assertEqual(roman2numeric('VII'), 7)
  def test_eight(self):
    self.assertEqual(roman2numeric('VIII'), 8)
  def test_eleven(self):
    self.assertEqual(roman2numeric('XI'), 11)
  def test_twenty(self):
    self.assertEqual(roman2numeric('XX'), 20)
  def test_30(self):
    self.assertEqual(roman2numeric('XXX'), 30)

  def test_60(self):
    self.assertEqual(roman2numeric('LX'), 60)
  def test_70(self):
    self.assertEqual(roman2numeric('LXX'), 70)
  def test_80(self):
    self.assertEqual(roman2numeric('LXXX'), 80)

  def test_200(self):
    self.assertEqual(roman2numeric('CC'), 200)
  def test_300(self):
    self.assertEqual(roman2numeric('CCC'), 300)
  def test_600(self):
    self.assertEqual(roman2numeric('DC'), 600)
  def test_800(self):
    self.assertEqual(roman2numeric('DCCC'), 800)

  def test_1100(self):
    self.assertEqual(roman2numeric('MC'), 1100)
  def test_1200(self):
    self.assertEqual(roman2numeric('MCC'), 1200)

  def test_eighteeneityeight(self):
    self.assertEqual(roman2numeric('MDCCCLXXXVIII'), 1888)
  def test_twothousand(self):
    self.assertEqual(roman2numeric('MM'), 2000)


class TestRome2NumericComplexCase(unittest.TestCase):
  def test_four(self):
    self.assertEqual(roman2numeric('IV'), 4)
  def test_nine(self):
    self.assertEqual(roman2numeric('IX'), 9)
  def test_14(self):
    self.assertEqual(roman2numeric('XIV'), 14)
  def test_nineteen(self):
    self.assertEqual(roman2numeric('XIX'), 19)
  def test_fourty(self):
    self.assertEqual(roman2numeric('XL'), 40)
  def test_ninety(self):
    self.assertEqual(roman2numeric('XC'), 90)
  def test_ninetythree(self):
    self.assertEqual(roman2numeric('XCIII'), 93)
  def test_ninenine(self):
    self.assertEqual(roman2numeric('XCIX'), 99)

  def test_400(self):
    self.assertEqual(roman2numeric('CD'), 400)
  def test_900(self):
    self.assertEqual(roman2numeric('CM'), 900)


  def test_nineninenine(self):
    self.assertEqual(roman2numeric('CMXCIX'), 999)
    #self.assertRaise(roman2numeric('IM'), 999) #???

  def test_nineteenninetynine(self):
    self.assertEqual(roman2numeric('MCMXCIX'), 1999)
roman.py
import StringIO


def nullimple(s):
  return 0


'''
  cfg

  R -> Letter*
  Letter -> 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M'
  # too large

  Basic/Combined Level
  'M'*'D'?'C'{0,3}'L'?'X'{0,3}'V'?'I'{0,3}


'''
def CombinedLevel(s):
  r = 0
  for d in s:
    if d == 'M':
      r += 1000
    elif d == 'D':
      r += 500
    elif d == 'C':
      r += 100
    elif d == 'L':
      r += 50
    elif d == 'X':
      r += 10
    elif d == 'V':
      r += 5
    elif d == 'I':
      r += 1
    else:
      raise 'error'
  return r



def ComplexLevel(s):
  '''
  cfg

  R -> thousands hundreds tens ones
  thousands -> 'M' thousands | empty
  hundreds -> 'C' 'M' | range500to800 | 'C''D' | range000to300
  range500to800 -> 'D' range000to300
  range000to300 -> 'C'{0, 3}
  tens -> 'X' 'C' | range50to80 | 'X' 'L' | range00to30
  range50to80 -> 'L' range00to30
  range00to30 -> 'X'{0,3}
  ones -> 'I''X' | range5to8 | 'I''V' | range0to3
  range5to8 -> 'V' range0to3
  range0to3 -> 'I'{0,3}

  '''
  global lookahead, result #ugh!
  result = 0

  buf = StringIO.StringIO(s)
  lookahead = None


  def roman():
    ''' R -> thousands hundreds tens ones '''
    global lookahead
    lookahead = buf.read(1)
    while True:
      if lookahead == 'M':
        thousands()
      else:
        break
    if lookahead in ('C', 'D'):
      hundreds()
    if lookahead in ('X', 'L'):
      tens()
    if lookahead in ('V', 'I'):
      ones()

  def thousands():
    global result
    match('M')
    result += 1000

  def hundreds():
    ''' hundreds -> 'C' 'M' | range500to800 | 'C''D' | range000to300 '''
    global result
    p = peek()
    if p == 'M':
      match('C')
      match('M')
      result += 900
      return
    elif p == 'D':
      match('C')
      match('D')
      result += 400
      return
    elif lookahead == 'D':
      range500to800()
    else:
      range000to300()
    return

  def range500to800():
    global result
    match('D')
    result += 500
    range000to300()

  def range000to300():
    global result
    while True:
      if lookahead == 'C':
        match('C')
        result += 100
      else:
        break
  def tens():
    ''' tens -> 'X' 'C' | range50to80 | 'X' 'L' | range00to30'''
    global result
    p = peek()
    if p == 'C':
      match('X')
      match('C')
      result += 90
      return
    elif p == 'L':
      match('X')
      match('L')
      result += 40
      return
    elif lookahead == 'L':
      range50to80()
    else:
      range00to30()

  def range50to80():
    global result
    match('L')
    result += 50
    range00to30()

  def range00to30():
    global result
    while True:
      if lookahead == 'X':
        match('X')
        result += 10
      else:
        break

  def ones():
    ''' ones -> 'I''X' | range5to8 | 'I''V' | range0to3 '''
    global result
    p = peek()
    if p == 'X':
      match('I')
      match('X')
      result += 9
      return
    elif p == 'V':
      match('I')
      match('V')
      result += 4
      return
    elif lookahead == 'V':
      range5to8()
    else:
      range0to3()

  def range5to8():
    ''' range5to8 -> 'V' range0to3 '''
    global result
    match('V')
    result += 5
    range0to3()

  def range0to3():
    ''' range0to3 -> 'I'{0,3} '''
    global result
    while True:
      if lookahead == 'I':
        match('I')
        result += 1
      else:
        break

  def match(t):
    global lookahead
    if lookahead == t:
      lookahead = buf.read(1)
    else:
      error('expected %s, but got %s'%(t, lookahead))

  def peek():
    #print 'peek pos%d %s'%(buf.tell(), buf.getvalue())
    r = buf.read(1)
    if r:
      buf.seek(-1, 1)
    #print 'peek pos%d %s'%(buf.tell(), buf.getvalue())
    #print 'read:', repr(r)
    return r

  def error(msg):
    raise Exception(msg)

  roman()

  return result

def roman2numeric(s):
  #return nullimple(s)
  #return CombinedLevel(s)
  return ComplexLevel(s)

2011年7月6日水曜日

写経 p87

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
いろいろ間違えた。


import StringIO



class Token:
  def __init__(self, raw):
    self.raw = raw

  def __eq__(self, x):
    assert isinstance(x, Token)
    return self.raw == x.raw


class Num(Token):
  def __init__(self, raw):
    Token.__init__(self, raw)
    self.value = int(raw)

class Identifier(Token):
  pass

class Div(Token):
  pass

class Mod(Token):
  pass

class Done(Token):
  pass



buf = None
lineno = None
lookahead = None
symtable = dict()


def lexan():
  while True:
    t = buf.read(1)
    if t.isspace():
      pass #skip white spaces
    elif t == '\n':
      lineno = lineno + 1
    elif t.isdigit():
      tokenval = None
      tokenval = int(t)
      t = buf.read(1)
      while t.isdigit():
        tokenval = tokenval * 10 + int(t)
        t = buf.read(1)
      buf.seek(-1, 1)
      return Num(tokenval)
    elif t.isalpha():
      lexbuf = ''
      while t.isalpha() or t.isdigit():
        lexbuf += t
        t = buf.read(1)
        # no size error...
      # lexbuf += EOS python string do not need explicit \0
      if t != '':
        buf.seek(-1, 1)

      # it is better idea to tweek constructor of Identity as a flyweight pattern?
      try:
        i = symtable[lexbuf]
      except KeyError:
        i = Identifier(lexbuf)
        symtable[lexbuf] = i
      return i

    elif t == '': #EOF
      return Done(t)

    else:
      # tokenval = None
      return Token(t) #, tokenval)


def parse():
  global lookahead
  lookahead = lexan()
  while not isinstance(lookahead, Done):
    expr()
    match(';')

def expr():
  global lookahead
  t = None
  term()
  while True:
    if lookahead.raw in ('+', '-'):
      t = lookahead
      match(lookahead)
      term()
      emit(t)
      continue
    else:
      return

def term():
  global lookahead
  factor()
  while True:
    if lookahead.raw in ('*', '/') or isinstance(lookahead, (Div, Mod)):
      t = lookahead
      match(lookahead)
      factor()
      emit(t)
      continue
    else:
      return


def factor():
  global lookahead
  if lookahead.raw == '(':
    match('(')
    expr()
    match(')')
  elif isinstance(lookahead, Num):
    emit(lookahead)
    match(lookahead)
  elif isinstance(lookahead, Identifier):
    emit(lookahead)
    match(lookahead) #Identifier)
  else:
    error("syntax error")
def match(t):
  global lookahead
  if isinstance(t, Token):
    if lookahead == t:
      lookahead = lexan()
    else:
      error("syntax error")
  elif isinstance(t, str):
    if lookahead.raw == t:
      lookahead = lexan()
    else:
      error('lookahead is %s, match arg is %s'%(lookahead.raw,  t))
  else:
    error("syntax error")

def error(msg):
  raise msg + ' at %d'%(lineno)


def emit(t):
  if False:
    pass
  elif isinstance(t, Div):
    print 'Div'
  elif isinstance(t, Mod):
    print 'Mod'
  elif isinstance(t, Num):
    print '%d'%(t.value)
  elif isinstance(t, Identifier):
    print t.raw
  else:
    print "token %s"%(t.raw)



def init():
  global symtable
  symtable['div'] = Div('div')
  symtable['mod'] = Mod('mod')


buf = StringIO.StringIO('2+3 * 5;\n 12 div 7 mod 2;')
lineno = 1

init()
parse()




2011年7月5日火曜日

唐突に写経

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
いつまでつづくのやら。

import StringIO
buf = StringIO.StringIO('9-5+2')

lookahead = ''

def expr():
  term()
  while True:
    if lookahead == '+':
      match('+')
      term()
      print '+',
    elif lookahead =='-':
      match('-')
      term()
      print '-',
    else:
      break


def term():
  if lookahead.isdigit():
    print lookahead,
    match(lookahead)
  else:
    error()

def match(t):
  global lookahead
  if lookahead == t:
    lookahead = buf.read(1)
  else:
    error()

def error():
  print 'syntax error'
  raise



lookahead = buf.read(1)
expr()
print '\n'

2011年5月14日土曜日

Kindle DXに手垢をつける話

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
私は日本語が母語なので、ここを参考に日本語フォントを導入することにした。

どのフォントがどこに使われるかが大事なわけで、その対応関係はここによると

Sans_Regular.ttf: Used in the Settings & Experimental page for the explanation texts. Also for the dictionnary/note popups.
Sans_Bold.ttf: Used in the menus, in the top & bottom panel, and as the collection title when browsing a collection, and the inline text in the book & collection browsers (ie. the 'delete this item' stuff & co)
Sans_Italic.ttf: May be used alongside Sans_Regular or Sans_Bold somewhere ^^
Sans_BoldItalic.ttf: Used in the book list, for the collection names

Serif_*.ttf: Used in the reader, that's the font your books will be rendered with.
Serif_Bold.ttf: Used in the book & collection browser for the book title & author. (NOTE: For these purposes, Serif_Regular is used instead on FW 3.x)

とのこと。

そこで、こんな感じの対応関係にした。
Sans_Regular.ttf <==> meiryo-UI.ttf
Sans_Bold <==> meiryo-UI-bold.ttf
Sans_Italic <==> meiryo -UI italic
Sans_BoldItalic <==> meiryo-ui italic bold

Serif_Regular <==> meiryo
Serif_Italic <==> meiryo Italic
Serif_Bold <==> meityo bold
Serif_BoldItalic <==> meityo bolditalic 
meiryoの入手なのだが、手持ちのwindows7からttcを取り出し(ファイルを検索するだけ)、ttfに分割。
osakaがいい人はMacから取り出せばよいだろう。この辺は好み次第だが、UI用のものがUIに使われるようにしたほうが筋がよいと個人的には思う。

別に何も難しいことはなかった。ただなぜかtextの表示が遅い。固まっているように感じられる。
仕上がりはこんな感じ。

cubepdfとかでpdf化して持ってくるほうが本筋だと思う。青空文庫はwebアプリが存在してpdfにコンバートしてくるのでそれを持ってくるのがよい。
余談だが、cubepdfはGPL v3のようだ。誰かsourceをrequestした人はいるのだろうか?利益モデルはbaiduのアフィリアプリなのだが、GPLということでstripしてしまうこともできるだろう。まあ、optoutできるからやる必要も無いのだが。

2011年3月4日金曜日

リーグ戦による選抜

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
JBL rating 1200が1000のなかで25pt matchで一人選抜する場合をモンテカルロしてみた結果。
試行回数は1サイズあたり10000.

from random import random
from bglib.stat.rating import  winning_chance

'''
one 1200.0 in  999 1000.0 league
'''


class League:
  def __init__(self, ratings):
    self.result = dict()
    self.size = len(ratings)
    self.ratings = ratings

  def report(self, i, j, r):
    assert i in range(self.size)
    assert j in range(self.size)
    assert isinstance(r, bool)
    self.result.update({(i, j):int(r)})
    self.result.update({(j, i):int(not r)})

  def find_winner(self):
    winner = 0
    winners_pt = 0
    for i in range(self.size):
      pt = 0
      for j in range(self.size):
        if i==j:
          continue
        pt += self.result[(i, j)]
      if pt >= winners_pt:
        winner = i
        winners_pt = pt
    return winner, winners_pt


  def run(self):
    for i, x in enumerate(self.ratings):
      for j in range(i + 1, self.size):
        y = self.ratings[j]
        wc = winning_chance(x, y, 25)
        assert x >= y
        self.report(i, j, wc > random())

def simulate(sizer):
  N = 10000
  count = 0
  for trial in range(N):
    league = League((1200.0,)+(1000.0,)*sizer)
    league.run()
    w = league.find_winner()
    if w[0] == 0:
      count+=1
  print sizer, count

#for sizer in range(1, 100):
simulate(100)
サイズ(1000の人の人数)は1から20まで。
結果は10000回あたり1200が選抜された回数。

数字を眺めた感覚としては+-50くらいの精度はありそうですね。
1 7615
2 5901
3 4362
4 4313
5 4449
6 4361
7 4468
8 4685
9 4800
10 4866
11 5079
12 5197
13 5263
14 5572
15 5680
16 5838
17 5929
18 6144
19 6267
20 6427

100人いる場合は995回 out of 1000回だった。1000人はやるかちなさそう。
次の結果の試行回数は1000回
15 569
20 634
25 701
30 786
35 809
40 875
45 884
50 920

2011年1月26日水曜日

sqlalchemy

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
適当にはじめてみた。
#!python

import sys
print sys.version

import sqlalchemy
print sqlalchemy.__version__
#0.3.11

from sqlalchemy import create_engine
from sqlalchemy import Column, MetaData, Table, types, ForeignKey
from sqlalchemy.orm import mapper, relation

metadata = MetaData()

player = Table('player', metadata,
    Column('id', types.Integer, primary_key=True),
    Column('name', types.String(100)),
    Column('email', types.String(256))
)


match = Table('match', metadata,
    Column('id', types.Integer, primary_key=True),
    Column('venue', types.String(100)),
    Column('length', types.Integer),
    Column('player0', types.Integer, ForeignKey('player.id')),
    Column('player1', types.Integer, ForeignKey('player.id')),
    Column('result', types.Enum('0won', '1won')),
)


engine = create_engine('sqlite:///:memory', echo=True)

metadata.create_all(engine)

実行結果。
[nori@nagato]~/Desktop/work/ratingdb% python2.7 model.py
2.7.1 (r271:86832, Jan 26 2011, 22:14:26)
[GCC 4.1.2 20080704 (Red Hat 4.1.2-48)]
0.6.6
2011-01-26 23:48:23,370 INFO sqlalchemy.engine.base.Engine.0x...49d0 PRAGMA table_info("player")
2011-01-26 23:48:23,370 INFO sqlalchemy.engine.base.Engine.0x...49d0 ()
2011-01-26 23:48:23,370 INFO sqlalchemy.engine.base.Engine.0x...49d0 PRAGMA table_info("match")
2011-01-26 23:48:23,370 INFO sqlalchemy.engine.base.Engine.0x...49d0 ()
2011-01-26 23:48:23,371 INFO sqlalchemy.engine.base.Engine.0x...49d0
CREATE TABLE player (
        id INTEGER NOT NULL,
        name VARCHAR(100),
        email VARCHAR(256),
        PRIMARY KEY (id)
)


2011-01-26 23:48:23,371 INFO sqlalchemy.engine.base.Engine.0x...49d0 ()
2011-01-26 23:48:23,386 INFO sqlalchemy.engine.base.Engine.0x...49d0 COMMIT
2011-01-26 23:48:23,387 INFO sqlalchemy.engine.base.Engine.0x...49d0
CREATE TABLE "match" (
        id INTEGER NOT NULL,
        venue VARCHAR(100),
        length INTEGER,
        player0 INTEGER,
        player1 INTEGER,
        result VARCHAR(4),
        PRIMARY KEY (id),
        CHECK (result IN ('0won', '1won')),
        FOREIGN KEY(player1) REFERENCES player (id),
        FOREIGN KEY(player0) REFERENCES player (id)
)


2011-01-26 23:48:23,387 INFO sqlalchemy.engine.base.Engine.0x...49d0 ()
2011-01-26 23:48:23,391 INFO sqlalchemy.engine.base.Engine.0x...49d0 COMMIT