Date: 2009-07-30
Tags: python, programming, web

buildoutで開発3: easy_install できるように公開する

eggの作り方が分からない, buildoutで開発1: WSGIアプリをeggで作る, buildoutで開発2: buildoutで環境を整える の続き。buildoutというよりsetuptoolsネタ。

buildoutで自動的にパッケージを取ってくる仕組みは内部的にsetuptools/easy_install.pyを使用していて、setuptoolsはデフォルトでは対象パッケージ名を pypi (Python Package Index) に探しに行く。pypiへの登録は python setup.py register で出来るんだけど、前回作ったようなwsgiappは実験用なので登録したくないし、仕事で作っているパッケージ類は世界に公開してはまずい。ということで、easy_installの --find-links (-f) オプションでpypi以外のページを指定する方法でpypiに公開しなくても自動インストール出来るようにする。

(setuptoolsのマニュアルを読むと、そのまんまな記載がある: Making your package available for EasyInstall, Sumiさんの日本語訳)

前提

  • pypiに登録しない

  • wsgiappをSubversionから取ってくる

Subversionリポジトリをhttpで公開する

SVNの公開方法は file:, svn:, http: とあるけど、easy_installの-fオプションではページ内のリンクにfile:と書いてあってもSVNとして認識されないので、素直にhttp:を使うことにする。あと、SVN以外のリポジトリも使えないっぽい。

ということで、httpdにmod_dav_svnで設定して公開する。URLは http://localhost:8080/repos でSVNのルートが見えるようにしておく。

easy_install用のindexページを用意する

easy_installの-fオプションは指定したページのリンクをチェックして、リンクURLがegg用リンクであれば使ってくれる。eggとして認識されるリンクは、URL末尾に #egg=projectname-version という記載があればよいので、開発版wsgiappの場合は http://localhost:8080/repos/wsgiapp/trunk#egg=wsgiapp-dev となっていればeasy_installで認識してくれるようになる。

ということで、http://localhost:8080/index.htmlというページを作って上記URLをaタグで書いておく。

easy_installでインストールしてみる

実際に認識されるかどうか確認する。確認したいだけなので、 --dry-run オプションを付けて実行。dry-runのせいで最後はエラーになるけど、途中までは以下のようにsubversionからcheckoutしてくれて、index.htmlがうまく働いている事が確認出来た。

ここまでくれば、index.htmlを自動生成するようにwsgiappを改造すれば後々楽になりそう。

wsgiappで動的にPackageIndexページを生成する

とりあえずてきとーに、'指定PATH/package名/trunk'を'package名#egg=package名-dev'としてリンクするように、 buildoutで開発2: buildoutで環境を整える で作成したwsgiapp.scraperに新しい関数を追加する。

wsgiapp/scraper.py の追加関数部分:

import urlparse
import string

def eggifyLinks(fileobj, basepath=''):
    """\
    eggifyLinks read html data from given fileobj and modify href
    attributes.

        >>> from StringIO import StringIO
        >>> fileobj = StringIO('''\
        ... <html><body><a href="wsgiapp">wsgiapp</a></body></html>
        ... ''')
        >>> content = eggifyLinks(fileobj)
        >>> 'href="wsgiapp/trunk#egg=wsgiapp-dev"' in content
        True

    ``basepath`` param effect for relative url.

        >>> fileobj = StringIO('''\
        ... <html><body><a href="wsgiapp">wsgiapp</a></body></html>
        ... ''')
        >>> content = eggifyLinks(fileobj, 'http://domain/sub/')
        >>> 'href="http://domain/sub/wsgiapp/trunk#egg=wsgiapp-dev"' in content
        True

    if href ends with '/', eggifyLinks return same result.

        >>> fileobj = StringIO('''\
        ... <html><body><a href="wsgiapp/">wsgiapp</a></body></html>
        ... ''')
        >>> content = eggifyLinks(fileobj)
        >>> 'href="wsgiapp/trunk#egg=wsgiapp-dev"' in content
        True

    work with full url.

        >>> fileobj = StringIO('''\
        ... <html><body><a href="http://localhost:8080/repos/wsgiapp/">wsgiapp</a></body></html>
        ... ''')
        >>> content = eggifyLinks(fileobj)
        >>> 'href="http://localhost:8080/repos/wsgiapp/trunk#egg=wsgiapp-dev"' in content
        True

    if url have #id, href is not modified.

        >>> fileobj = StringIO('''\
        ... <html><body><a href="wsgiapp#foo">wsgiapp</a></body></html>
        ... ''')
        >>> content = eggifyLinks(fileobj)
        >>> 'href="wsgiapp#foo"' in content
        True
        >>> '#egg' not in content
        True

    if url have no package name, href is not modified.

        >>> fileobj = StringIO('''\
        ... <html><body>
        ... <a href="..">Parent</a>
        ... <a href="http://domainonly/">domain</a>
        ... </body></html>
        ... ''')
        >>> content = eggifyLinks(fileobj)
        >>> 'href=".."' in content
        True
        >>> 'href="http://domainonly/"' in content
        True
        >>> '#egg' not in content
        True

    """
    baseparts = urlparse.urlparse(basepath)

    bs = BeautifulSoup(fileobj)
    for elem in bs.findAll('a'):
        if elem.has_key('href'):
            href = elem['href']
            parts = list(urlparse.urlparse(href))

            # #id check
            if parts[5]:
                continue # #id already exist

            # modify path
            path = parts[2]
            if path.endswith('/'):
                path = path[:-1]
            pkgname = path.split('/')[-1]
            if not pkgname or pkgname[0] not in string.letters:
                continue # pkgname does not seem package name
            parts[2] = '%(path)s/trunk#egg=%(pkgname)s-dev' % locals()

            # modify domain
            if basepath and not parts[1]:
                parts[0] = baseparts[0]
                parts[1] = baseparts[1]
                if parts[2][0] != '/':
                    p = baseparts[2]
                    if p.endswith('/'):
                        p = p[:-1]
                    parts[2] = p + '/' + parts[2]

            # update href
            elem['href'] = urlparse.urlunparse(parts)

    return bs.prettify()

テストする。

呼出元を新しい関数に変更。

wsgiapp/startup.py の変更部分:

import urllib2

def application(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/html')]
    start_response(status, response_headers)
    return [scraper.eggifyLinks(
        urllib2.urlopen("http://localhost:8080/repos/"),
        "http://localhost:8080/repos/",
    )]

実際に動作させた時の出力を bin/paster request wsgi.ini / で確認。

easy_installでうまく動くか確認するため、wsgiappをサーバー動作させてから、別コンソールでeasy_installを-fオプション付きで動かしてみてwsgiappパッケージを見つけられれば成功。8080ポートはapacheで使ってるので8180で起動するようにwsgi.iniを変更しておく。

dry run なのでsetup.pyの実行には失敗する。実際にインストールする場合は-nを外して実行してみよう。

あとは、このwsgiappをmod_wsgiで動作するようにしておけば、超簡易版のローカル用パッケージ一覧生成ツールとして使える。使えるといいなぁ。

もっとちゃんとやろうと思ったら、pysvn等でパッケージの一覧を取ってきて、各パッケージのtrunkのURLに、 #egg=パッケージ名-dev と付けたり、tagsから自動で #egg=パッケージ名-tag名 としてみたりすればいいんだけど、毎回動的にやってると重いし、そこまでやるんだったらローカルにPyPIを立ち上げた方が良いと思う。作り方は EggBaskethow to run your own private PyPI (Cheeseshop) server << Fetchez le Python を参考すればよさそう。