Git+Sphinxでサイト管理のベストプラクティスがわからないなぁというはなし

@ando_ando_andoに呼ばれてコミュニティfでSphinxサイト構築の手伝いをしてきた。彼のためのGit入門サイトが出来てたのを本人に教えてもらったのだけど、alias切ってなくてstatusとかcommitとかフルで手打ちしてたので、上の4つくらいはやっておいたほうがいいんじゃないかなぁ(と今思った)。

ついでに、@ando_ando_andoの「静岡の東部にはHaskellerが多い」という主張を検証するために、今週末に香香飯店あたりで集まって飲むことになったので、参加される方がいれば連絡してください。

Git+Sphinxでサイト管理

さて、彼はインフラエンジニアなので、作業メモとかblockdiagで描いた図なんかを手元で編集して、さくらのVPSで管理したいそうだ(GithubPagesでいいじゃんって言ったら、認証かけたいからそれはダメらしい)。

サイト構成

  • _build/html/*もGit管理
  • 共有リポジトリはベア(git init --bare)

という状態にして、サーバー側にGitの共有リポジトリ置いて、プッシュしたタイミングで公開サイトのほうも更新するようにフックを設定しておけばいいよねーという僕の提案に対し

サイト構成

pushしたらpullするようにフックを設定すると、もとのrstとかMakefileがサイトのディレクトリに含まれてしまい美しくない。_build/html以下のファイルだけを公開サイトに置きたいと言い出した

ここから、ちょっとハマった。

bareをやめる(失敗)

_build/html以下のみを公開するんだったら、サーバー側にpushした後に、一回cloneとかpushして作業ディレクトリを作ってからcpするなりしないといけないということなので、最初からbareオプション付けないで共有リポジトリ設定すればいいんじゃないと。

これは見事に失敗する

remote: error: refusing to update checked out branch: refs/heads/master
remote: error: By default, updating the current branch in a non-bare repository
remote: error: is denied, because it will make the index and work tree inconsistent
remote: error: with what you pushed, and will require 'git reset --hard' to match
remote: error: the work tree to HEAD.

メッセージ見たら、まぁそうだよなと思った。なので、これは却下した。

一回別の場所にclone(pull)してその後必要な物だけcp

結局サーバーにもクローンを作って_build/html/*を/var/www/html以下の適当なディレクトリにコピーするようにした(今ココ)。

サイト構成

手でコマンド叩いたけど、フックに書いておけばいいかな。

scpする(番外)

単にbuildしたやつをscpすればいいんじゃない?ってことで

サイト構成

を提案してみたが、彼の美意識にそぐわなかったのかボツった。

まとめ

cpするためだけにサーバー側に作業リポジトリを作っておくというのはなんとなく気持ち悪いんだけど、他にいい方法ないのかな?

Makefile書き換えて/var/www/htmlにbuildするようなオプション付けておけば、そもそも_buildをGitで管理しなくていいだろうとも思うんだけど、そこらへんの管理のさじ加減みたいなのもちょっと自信がない。

ToDo

Pythonプロフェッショナルプログラミングになんかヒントっぽいもの書いてないかなぁと読みなおす。

ProductName Pythonプロフェッショナルプログラミング
ビープラウド
秀和システム / 2940円 ( 2012-03-26 )


参考

ProductName Gitポケットリファレンス
岡本 隆史
技術評論社 / 2604円 ( 2012-07-10 )


ProductName エキスパートPythonプログラミング
Tarek Ziade
アスキー・メディアワークス / 3780円 ( 2010-05-28 )


PyConJPのSphinxのセッションのスライドを見た

2日目の途中で帰ってしまったので、朝コーヒーを飲みながら、残りのスライドを眺めてた。

HTMLテーマの拡張の話Docutilsが参考になった。

そういえば二年くらい前に原稿書くときに使ったときはwordに出力するのに難儀したけど、今はdocxに出力できるらしいので、MSWORDな会社でも安心して使えますね。さらにmoin2を組み合わせれば、普段はWikiで使いつつ、必要に応じてSphinx経由で好きな書式で文書出力ってのもやりやすくなるだろうし。

SQLAlchemyのmany-to-manyであるタグを含むエントリをフィルタする方法

単にタグを含むエントリを取ってくる場合にはこうやればいいんだけど、さらに絞り込む場合にどうやればいいのか悩んだ。

filter(Entry.tags.in_(tags))

とかやったら、

"in_()" operator is not currently implemented for many-to-one-relations

みたいなエラーが出てきたので、グーグル先生にお伺いを立てたら解答が見つかった。

というわけで

Entry.query.filter_by(status=1).filter(Entry.tags.any(Tag.id.in_([tag.id])))

という感じで、anyとin_を組み合わせるのがいいらしい。

もう少し精進せなアカンなと。

ProductName Essential Sqlalchemy
Rick Copeland
Oreilly & Associates Inc / 2556円 ( 2008-06 )


結局こういったあたりで悩むんだったら、最初からpymongoでいいんじゃなかろうかと思ったりするんだけど、Flask-SQLAlchemyが便利すぎなのでなかなか悩ましいところ。

Python Testing Cookbook

Python Testing: Beginnerと同時期に買って一緒に読んだのだけど、書評を書いてなかったので今更ながら書いてみた。

ProductName Python Testing Cookbook
Greg Lee Turnquist
Packt Publishing / 3770円 ( 2011-05-30 )


Python Testing: BeginnerはユニットテストとかTDDの習得に主眼を置いているのに対し、本書はもうちょっと広くて高度な内容を取り扱っている。

BDDとか受け入れテストとか(Jenkinsを使った)継続的インテグレーションとかテストのカバレッジとかスモークテストなんか。

最終章の「良いテストの習慣」はなかなかいい言葉がまとまっている(ような気がする)

  • Something is better than nothing
  • Coverage isn't everything
  • Be willing to invest in test fixtures
  • Harvesting metrics

こんなかんじで12の習慣が。

個人的に良かったのがBDDの章であった。本書ではnose+mockitで説明されてた。ちなみに僕はpyVowsを使っています(Node.jsにVowsがあるので)。

それからlettuceの説明もよかった。これはちょっと試しただけで全然使ってないけど、そろそろ真面目につかうことになりそうだ。

というわけで、両方とも役に立っている。

ProductName Python Testing: Beginner's Guide
Daniel Arbuckle
Packt Publishing / 3220円 ( 2010-01-31 )


WIPOで公開番号からpdfを落とす

公開番号.pdfとかなってるんだろうから楽勝だろうなと思ったら、そんなことはなくてドキュメント用のIDがついていて面倒くささが20%増量してた。

検索してpdfのリンクをスクレイプしてダウンロードするようにしておいた。

こんな感じ

import sys
import requests
from pyquery import PyQuery as pq

wipo_url = "http://patentscope.wipo.int/"

def get_pdf_url(wipoid):
    url = wipo_url + "search/en/detail.jsf?docId=" \
        + wipoid + "&recNum=1&tab=PCTDocuments"
    d = pq(requests.get(url).content)
    pdf_link = pq(d('table.rich-table:eq(1) a:contains("PDF")')[1]).attr('href')
    return wipo_url + pdf_link

if _name_ == '__main__':
    wipoid = sys.argv[1]
    pdf_url = get_pdf_url(wipoid)
    with open(wipoid + ".pdf", "wb") as f:
        f.write(requests.get(pdf_url).content)

pyqueryがちょっと決め打ちしすぎなのと、requestsでgetしたのをファイル開いて書き出してるんだけどsaveみたいなメソッドないのかな?

PyConJP2012二日目

聞いたセッションは以下

基調講演の緊張感が面白かった。YAPCなんかでは他言語比較はよく見た気がするけど、PyConでPerlやRubyと比較してた。個人的にPythonにも正規表現リテラル欲しいなぁと思っているので、そこは同意。

基調講演後はSphinxの部屋にいたんだけど、電車の関係で早めに帰った。後半はディープな話題に入っていったのかなぁ、それとも初心者向けの話題が多めだったのか気になる。後でスライド見ようっと。自分用のHTMLテーマとか作りたいし、職場のレポート自動化にSphinx使えないかなぁと思っているので、そのうちいじってみようっと。

そういえば、SphinxのソースコードリーディングメモをSphinxで管理してるけどpyccoのほうがイイかもしれないなぁ。

PyConJP2012一日目

PyConJP2012の一日目は超楽しかった。

python+awsのepubを買おうか迷っていたら、PyConJP用に薄い本として特別に出版されていたので即ゲットしておいた。

1347712165

僕はこのブログをFlask+gunicorn+supervisorで動かしているのと、職場のウェブサービスも似たような構成にしているので、今日の全てのセッションが楽しかったし勉強になった。

基調講演

マシン名がナウシカ。

Python Type and Object

あとで、Python Types and Objectsを丁寧に読みなおそうと思った。

演者のスライドにjavascriptとpythonのクラスシステムを比較したのもあって、それも面白そう。

MongoDB with Python

Flask-SQLAlchemy+Flask-WTFが便利なのでなかなか移行する気になれないなぁ、でも気になってるんだよなぁという状況だったのだけど、ウェブサービスじゃなくて、ケモインフォマティクスの仕事でMongoDB使えそうじゃんと思ったので、来週会社に行ったら早速遊んでみる。距離のインデックスがちょっとよさげ。

発表内容もクックブック的なものも織り込まれていて良かった。スライド公開すんだろうか?して欲しいなぁ。

分散バージョン管理システムの組織化

ブランチ戦略がよくまとまっていて、かなりわかりやすかった。ただ僕はチーム開発してないので、最初の方に出てきたパターンで動いているのだが。

SQLAlchemyと僕

入門的な内容だったが、最後のツール紹介のAlembicってのは初めて知ったけど、そのうち使うことになりそうなので覚えておく。

Gunicorn what’s next? The new web challenge

gunicornの今後に関して。gunicorn便利ですよね。

Python 製ビルドツールのススメ

wafを積極的に使っていこうと思った。

みんなのPython 第3版

白地から赤地へと、ちょっと攻撃度が上がった。

ProductName みんなのPython 第3版
柴田 淳
ソフトバンククリエイティブ / 2940円 ( 2012-08-29 )


そろそろpython3に移行すべきか悩む。

まぁ、僕の場合はFlaskがサポートすればさくっと移行するような気がするが。

それまでは2.7系で頑張る。

pygamessも対応させないといけないなぁ。

日付の扱いを簡単にするPythonのパッケージ

そろそろ、うちの家庭菜園も秋植えの準備をしないといけないのに、お盆辺りから3週間くらい放っておいたら草ぼうぼうになってしまいげんなりしている。

さらに逆算したら大根の蒔きどきがギリギリなのでちょっと焦っている。石灰なんてまく余裕が無いので、このまま畝を立てて種まきしようかなどと思っている。

浅葱も植えてないし、8月は完璧に堕落したなぁ。葉物はやらんからいいけど。

さて、野菜を育てているとわかると思うのだが、収穫時期が

  • 定植してから100日後
  • 蒔いてから120日後

などと書いてあることが多いので、家庭菜園系のパイソニスタは日付計算に明るくないといけない。

>>> from datetime import datetime,timedelta
>>> datetime(2012, 5, 6) + timedelta(120)
datetime.datetime(2012, 9, 3, 0, 0)

こんな感じで収穫時期を見積もっているが、実際のところ覚えにくい。

そんなわけで、もうちょっと簡単に書けるwhenってのを見つけた。

これを使えば、今人参を撒くといつ収穫できるかとかさくっとわかる。

>>> import when
>>> when.future(days=110)
datetime.datetime(2012, 12, 23, 19, 55, 23, 107442)

今日撒くと年内くらいから収穫できる感じですかね。

まぁ、timedeltaの使い方を覚えてしまったのでいつものように計算してもいいかなぁ。

それよりも何を育てるか決めないと。

結合を切断しながらMatched Molecular Pair(MMP)を求める

論文読んでたら、コンベンショナルなMMPの求め方が載ってたので、似たようなやり方を考えてみた。

要するに網羅的に結合を切断していって

{Scaffold; [fragment, fragment, fragment]}

っていうハッシュを作っていく。fragmentは共通のScaffoldを持つので配列中の任意のフラグメントのペアがMMPということになる。

僕の切断ルール

僕の切断ルール

  • シングルボンド
  • 環の一部ではない
  • ボンドのどちらかの端がCであること

CHEMBL327743の結果

('CC[C@@H]([C@H](NC(=O)[C@@H]1CCCCN1CC(=O)*)/C=C/c1ccccc1)C', 'COc1cccc(c1*)OC', 'CHEMBL327743')
('CC[C@@H]([C@H](NC(=O)[C@@H]1CCCCN1C*)/C=C/c1ccccc1)C', 'COc1cccc(c1C(=O)*)OC', 'CHEMBL327743')
('CC[C@@H]([C@@H](/C=C/c1ccccc1)N*)C', 'COc1cccc(c1C(=O)CN1CCCC[C@H]1C(=O)*)OC', 'CHEMBL327743')
('CC[C@@H]([C@H](NC(=O)*)/C=C/c1ccccc1)C', 'COc1cccc(c1C(=O)CN1CCCCC1*)OC', 'CHEMBL327743')
('CC[C@@H](C(NC(=O)[C@@H]1CCCCN1CC(=O)c1c(OC)cccc1OC)*)C', '*/C=C/c1ccccc1', 'CHEMBL327743')
('COc1cccc(c1C(=O)CN1CCCC[C@H]1C(=O)N[C@H](/C=C/c1ccccc1)*)OC', 'CC[C@H](C)*', 'CHEMBL327743')
('*c1ccccc1', '*/C=C\\[C@H]([C@H](CC)C)NC(=O)[C@@H]1CCCCN1CC(=O)c1c(OC)cccc1OC', 'CHEMBL327743')
('*OC', 'CC[C@@H]([C@H](NC(=O)[C@@H]1CCCCN1CC(=O)c1c(*)cccc1OC)/C=C/c1ccccc1)C', 'CHEMBL327743')
('*OC', 'CC[C@@H]([C@H](NC(=O)[C@@H]1CCCCN1CC(=O)c1c(*)cccc1OC)/C=C/c1ccccc1)C', 'CHEMBL327743')
('*CC', 'COc1cccc(c1C(=O)CN1CCCC[C@H]1C(=O)N[C@@H](C(C)*)/C=C/c1ccccc1)OC', 'CHEMBL327743')
('CC[C@@H]([C@H](NC(=O)[C@@H]1CCCCN1CC(=O)c1c(OC)cccc1OC)/C=C/c1ccccc1)*', 'C*', 'CHEMBL327743')
('*C[C@@H]([C@H](NC(=O)[C@@H]1CCCCN1CC(=O)c1c(OC)cccc1OC)/C=C/c1ccccc1)C', 'C*', 'CHEMBL327743')

コード

どこで切断されたかどうかを明確にするために切断されたボンドにダミーアトム(atomicnumが0のアトム)をおいた。

canonicalSMILESで出力して.の文字でsplitした文字列をそのままキーに使っているが、openbabelのcanonicalizationアルゴリズムはMorgan Algorithmを改変したものを使っているのでおそらく大丈夫。

下のコードでは標準出力に書きだしたが、実際はデータベース化することになる。多分Mongo使う。

smi_string,id = clone.write(format="can")[:-1].split('\t')
frag1, frag2 = smi_string.split('.')
dbh.mmp.update({"core": frag1}, {"$push": {"frags": (frag2, id)}}, True)
dbh.mmp.update({"core": frag2}, {"$push": {"frags": (frag1, id)}}, True)

こんな感じだろうか。

以下MMPを求めるのに使ったコード

import pybel
import openbabel
import sys

input = sys.argv[1]

def mkclone(mol):
    obc = openbabel.OBConversion()
    obc.SetInAndOutFormats("mol", "mol")
    molstring = obc.WriteString(mol)
    new_mol = openbabel.OBMol()
    obc.ReadString(new_mol, molstring)
    return new_mol

def ring_member(rings, atom):
    for i, ring in enumerate(rings):
        if ring.IsMember(atom):
            return i
    return -1

def is_same_ring_member(rings, a1, a2):
    a1_idx = ring_member(rings, a1)
    a2_idx = ring_member(rings, a2)
    if a1_idx == -1 or a2_idx == -1:
        return False
    return a1_idx == a2_idx

def detect_del_bonds(mol):
    rings = mol.OBMol.GetSSSR()
    del_bonds = []
    for a in mol:
        if a.atomicnum == 6:
            for na in mol:
                n_a = na.OBAtom
                b = a.OBAtom.GetBond(n_a)
                if b != None and b.GetBondOrder() == 1 and not is_same_ring_member(rings, a.OBAtom, n_a):
                    if a.OBAtom.GetIndex() < n_a.GetIndex():
                        del_bonds.append((a.OBAtom.GetIndex(), n_a.GetIndex()))
    return del_bonds

for mol in pybel.readfile("sdf", input):
    chembl_id = mol.data["CHEMBL ID"]
    print chembl_id
    del_bonds = detect_del_bonds(mol)
    for a1, a2 in del_bonds:
        clone = pybel.Molecule(mkclone(mol.OBMol))
        # !!! index starts 0 but GetAtom starts 1 !!!
        ob_a1 = clone.OBMol.GetAtom(a1 + 1)
        ob_a2 = clone.OBMol.GetAtom(a2 + 1)
        bond = ob_a1.GetBond(ob_a2)
        clone.OBMol.DeleteBond(bond)

        new_atom1 = clone.OBMol.NewAtom()
        new_atom1.SetAtomicNum(0)
        new_bond1 = clone.OBMol.NewBond()
        new_bond1.SetBegin(ob_a1)
        new_bond1.SetEnd(new_atom1)
        new_bond1.SetBondOrder(1)

        new_atom2 = clone.OBMol.NewAtom()
        new_atom2.SetAtomicNum(0)
        new_bond2 = clone.OBMol.NewBond()
        new_bond2.SetBegin(ob_a2)
        new_bond2.SetEnd(new_atom2)
        new_bond2.SetBondOrder(1)

        smi_string, id = clone.write(format="can")[:-1].split('\t')
        frag1, frag2 = smi_string.split('.')
        #print (frag1, chembl_id)
        #print (frag2, chembl_id)
        print (frag1, frag2, chembl_id)