カテゴリおよびタグ一覧 アーカイブ

PyCon mini KumamotoでPythonistaの発表をしてきました

4/23に熊本で開催された PyCon mini Kumamoto 2017 でPythonistaの紹介をしてきました。

スライドは こちら に。

PythonistaはiOS上で動作するPython処理系でiOSアプリとして AppStore上で提供されています。

Pythonの標準モジュールのほか、iOSの機能をラップした独自モジュール、NumPy、SymPyなどがバンドルされており、 ちょっとしたスニペットからアプリまで幅広く書くことができます。

エディタやデバッガもアプリに含まれるのでPythonistaだけで開発することもできます。

iOSで開発したことある方は不思議に思われるかもしれませんが、 PythonistaはPythonインタプリタ上でコードを評価するのでコンパイル不要です。

コミュニティのツールを使えばGitも動きます。

手軽に手元で始められるPython生活をPythonistaでいかがでしょうか。 楽しいですよ。

Django 1.11がリリースされました

Django 1.11がリリースされました。

今回のリリースでは機能的にはそこまで目にとまるものはありませんでしたが、 LTSバージョンですので3年間公式でサポートされます。 Django 1.8のLTSを選んで使われていた方は次のバージョンとして選ばれるのではないでしょうか。

Django 1.11.xはDjangoの最後のPython 2シリーズのサポートになるので、 Django 2.0を目指しPython 3対応の優先度を上げていきましょう。

ワイアーチではDjangoやPythonで開発されたアプリケーションのサポート/コンサルトをおこなっています。 ご用命ありましたら、ご連絡くださいませ。

Swinjectのコンテナ階層について動作確認した

結論からいうと動いた。

$ swift -F . -target x86_64-apple-macosx10.12 main.swift
With parent container: Meow
With child container: Bow wow

みんな大好きDIコンテナ、その中でもあるといいなと思う機能の一つはモジュールの一部のみの差し替えですよね。 SwiftのDIコンテナであるSwinjectでもコンテナ階層( https://github.com/Swinject/Swinject/blob/master/Documentation/ContainerHierarchy.md )という文書化されていたので出来るかなと思い試してみました。

モジュールの一部のみ差し替えができると何がうれしいのか。 典型的なケースでは、ビジネスロジックが共通で使う外部サービスへのアダプタをDIするとき、ある特定のケースのみ特殊なアダプタに差し替えたい、という要求をDIの枠組みのなかだけで表現できるようになります。

以下、コードです。

import Swinject

protocol Animal {
    func sound() -> String
}

class Cat: Animal {
    func sound() -> String {
        return "Meow"
    }
}

class Dog: Animal {
    func sound() -> String {
        return "Bow wow"
    }
}

protocol Person {
    var pet: Animal { get }
}

class PetOwner: Person {
    let pet: Animal

    init(pet: Animal) {
        self.pet = pet
    }
}

let parent = Container()
parent.register(Animal.self) { _ in Cat() }
parent.register(Person.self) { r in
    PetOwner(pet: r.resolve(Animal.self)!)
}

let child = Container(parent: parent)
child.register(Animal.self) { _ in Dog() }

var person = parent.resolve(Person.self)!
print("With parent container:", person.pet.sound())

person = child.resolve(Person.self)!
print("With child container:", person.pet.sound())

サイトをリニューアルしました

Python mini hack-a-thon 夏山合宿 2016 にて、このサイトに記事を掲載する機能を追加いたしました。 いまのところQiitaに掲載したものを取り込むようにしています。

今回のリニューアルはNikolaというPython製の静的サイトジェネレータを使っています。

静的サイトジェネレータの特徴は

  • 入出力はファイルになるので管理や編集の手間が少なくなる。
  • セキュリティで気をつける箇所が少なくなる。
  • 動的に生成したコンテンツでの表現ができない。

このうち「動的に生成したコンテンツ」は主にWebアプリケーションで使われる手法ですが

  • コーポレート/メディアサイトでは動的に生成する要望があがるコンテンツは少ない。
  • SPAなどフロントエンド文化の発展によってブラウザ側で動的に表現したほうが能力が高いケースがでてきた。

これらの経緯があり、このたび採用にいたりました。 またNikolaを選んだ理由として、

  • Markdown, RestructuredText, HTMLほかで記事を書くことができるので、記事のボリュームや表現の幅など場面に合った書き方ができる。
  • 既存のコンテンツを変換することなく取り込むことができる。
  • コンテンツ自体と周りを構成するテンプレートが分離されており、テンプレートやテーマを容易にカスタマイズできる。

これらが挙げられます。

MySQL RouterでFabric Cacheプラグインを使うのがつらい

手元の環境はMySQL Router 2.0.3

なにがあったか

MySQL Fabricと一緒に使おうとMySQL RouterのFabric Cacheプラグインを有効にすると起動時に必ずMySQL Fabricのxmlrpcのパスワードを標準入力で聞いてくる。 設定ファイル上で設定する手段は提供されていないようにみえる。 コード上でもパスワードを回避するようなオプションはなさそうなところまでは確認した。

:mysql-router-2.0.3/src/fabric_cache/src/fabric_cache_plugin.cc

static int init(const AppInfo *info) {
  g_app_info = info;

  if (info && info->config) {

    if (info->config->get(kSectionName).size() > 1) {
      throw std::invalid_argument("Router supports only 1 fabric_cache section.");
    }

    for (auto &section: info->config->get(kSectionName)) {
      FabricCachePluginConfig config(section); // raises on errors
      fabric_cache::g_fabric_cache_config_sections.push_back(section->key);

      if (section->has("password")) {
        throw std::invalid_argument(
            "'password' option is not allowed in the configuration file. "
                "Router will prompt for password instead.");
      }

      auto password_key = make_cache_password(config.address, section->get("user"));
      if (have_cache_password(password_key)) {
        // we got the password already for this address and user
        continue;
      }
      // we need to prompt for the password
      auto prompt = mysqlrouter::string_format("Password for [%s%s%s], user %s",
                                               section->name.c_str(),
                                               section->key.empty() ? "" : ":",
                                               section->key.c_str(), config.user.c_str());
      auto password = prompt_password(prompt);
      fabric_cache_passwords.emplace(std::make_pair(password_key, password));
    }
  }

  return 0;
}

バックグラウンド起動しようとすると標準入力が切られるのでここで失敗して動作しない。 また、いやなことにrpmに同梱してあるsystemd向けのmysqlrouter.serviceも同様の問題を抱えている。

どう対応したか

標準入力から受けつける口しか用意していないのならば、標準入力から渡すしかない。 つまりこうなる。

:/etc/mysqlrouter/fabric_password

password
:/usr/lib/systemd/system/mysqlrouter.service

[Unit]
Description=MySQL Router
After=syslog.target
After=network.target

[Service]
Type=simple
User=mysql
Group=mysql

PIDFile=/var/run/mysqlrouter/mysqlrouter.pid

ExecStart=/bin/sh -c 'cat /etc/mysqlrouter/fabric_password | /usr/sbin/mysqlrouter'

PrivateTmp=true

[Install]
WantedBy=multi-user.target

Paramikoを使ってシェルスクリプトをリモートホストに流しこむ

SSHClient.exec_commandを使って一つずつコマンドを実行することはできるのですが、既存のシェルスクリプトを流しこみたいときどうしたらいいんだろうと試行錯誤しました。

TLDR

明示的にバッファをフラッシュするためにシェルスクリプトの終わりにexit\nを付ける。

作業ログ

import getpass
import paramiko

REMOTE_HOST = "YOUR_REMOTE_HOST"

c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
pkey = paramiko.RSAKey.from_private_key_file('/home/ubuntu/.ssh/id_rsa', getpass.getpass())
c.connect(REMOTE_HOST, username='ubuntu', pkey=pkey)

# こういう複数行のやつを流しこみたい
cmd = '''
/bin/ls /
/bin/touch /tmp/foo
echo 'foo
bar
baz' >> /tmp/foo
cat /tmp/foo
'''

まずbashを起動して標準入力で流しこむ方法を試しました。

stdin, stdout, stderr = c.exec_command('bash -')
stdin.write(cmd)
print(stdout.read())

しかし応答せず。 どこかでブロックしているのかと考え次にselectの方法を試しました。

import socket
import select
chan = c.invoke_shell()
chan.setblocking(False)

chan.send(cmd)
while True:
    r, _, _ = select.select([chan], [], [])
    if chan in r:
        try:
            x = chan.recv(1024)
            if len(x) == 0:
                break
            print(x)
        except socket.timeout:
            pass

これでも応答せず。ブロックしているというわけではなさそうなことが分かりました。 じゃあ何だろう、バッファかなと。 ローカルの送信側のバッファは明示的にフラッシュすればいいのですが、リモートホストのバッファをフラッシュする方法が分からず。調べてみたらシェルスクリプトを終了させれば自動的にフラッシュすると記述があったので、スクリプトを修正してみました。

cmd = '''
/bin/ls /
/bin/touch /tmp/foo
echo 'foo
bar
baz' >> /tmp/foo
cat /tmp/foo
exit
'''

動いた。なるほど。

まとめ

  • 同期非同期の問題ではなかった。
  • やりたいことが終わったらプロセスを落とす。シェルかソケットのバッファだろうがプロセス落とすときにはフラッシュされる。

AppiumでiPhone実機のMobileSafariをSelenium WebDriverで操作するまで

ios_webkit_debug_proxyだけで動かしてみた

インストール

今回はbrewからインストールした。

$ brew install ios_webkit_debug_proxy

動作確認

  1. 引数なしで起動すると今接続されているデバイスが自動で選択されlocalhost:9221で起動する。
  2. デバイス上でMobileSafariで適当なページを表示させる。
  3. Chromeでlocalhost:9221へ接続すると、接続しているデバイス、表示しているURLとリンクを辿れる。URLをクリックするとWebインスペクタが表示され、コンテンツをリモートで確認することができる。

Appiumを介してSelenium WebDriverでデバイスを操作する

Appiumのインストール

今回はbrewとnpmでインストールした。

$ brew install npm
$ npm install -g appium

もろもろインストールされる。

SafariLauncherをビルドしてappium-ios-driverがデバイスにインストールできるようにする

※ 本来はappium-ios-driverがSafariLauncherも自動的にビルドするようだがうまくいかなかったので先にSafariLauncherをビルドする。

  1. provisioning profileを作る。http://appium.io/slate/en/master/?ruby#setup67
  2. SafariLauncherを作ったプロファイルで動かしたいiOSバージョン向けにビルドする。 xcodebuild -sdk iphoneos9.2
  3. /usr/local/lib/node_modules/appium/node_modules/appium-ios-driver/build/SafariLauncherへSafariLauncher.appを配置する。

動作確認

  1. ios_webkit_debug_proxyがインストールされており正常に動作していること。
  2. Appiumをインストールする。
  3. Appiumをデバイスを使うように指定して起動する。appium --default-capabilities '{"app": "Safari", "deviceName": "iPhone", "udid": "${デバイスID}"}'
  4. Appiumがlocalhost:4723で起動しているので、Selenium WebDriverのRemoteドライバで接続する。
  5. Appiumが色々動いて、デバイス上でSafariLauncherが自動で起動し、Safariが自動で起動する。

こんなコードで操作できるようになる。

from selenium import webdriver

desired_capabilities = {
    'app': 'Safari',
    'platformName': 'iOS',
    'platformVersion': '9.2.1',
    'deviceName': 'iPhone',
}
driver = webdriver.Remote(
    command_executor='http://127.0.0.1:4723/wd/hub',
    desired_capabilities=desired_capabilities)
driver.get('http://yarch.jp')

AppEngine/PythonでDjangaeを試した

TokyoDjangoMeetup #6でAppEngineでDjango Adminを動かしたくて調査した記録です。

結論

  • Django non-relよりDjangaeのほうがよさそう
  • 制限付きではあるがDjango 1.8が、モデルAPIがそのまま動作する
  • Django Adminが動く!!!
  • Django REST Frameworkも動く!!!
  • settings.DJANGAE_ADDITIONAL_MODULES経由で他のランタイムで開発してるモジュールも一緒に動かせる、たとえば一部だけAppEngine/Goで開発とか

検証コード

ここにあります。

https://github.com/jbking/djangae-with-drf-demo

経緯

  • AppEngine/Pythonには前からDjangoサポートはあるのだがバージョンが1.5
  • しかもモデルAPIがAppEngineのDatastore APIやNDB Datastore APIを強いられる
  • いちおうCloud SQLでMySQLという選択肢はある
  • Django Adminが動かない
  • ModelFormとかももちろん動かない

作業履歴

  • Django non-relでDjango AdminをAppEngine/Pythonでとりあえず動かしたことがあるので詳しく調べ始めた
  • Django non-relはDjangoをforkしており1.7までしかサポートしていないことが判明
  • 世間ではLTSなDjango 1.8がメジャー、暗雲がたちこめはじめる
  • イベント主催者の一人にDjangaeを紹介される
  • ドキュメントを読んでみたら気になる一文が

https://djangae.readthedocs.org/en/latest/db_backend/

Previously, in order to use Django's ORM with the App Engine Datastore, django-nonrel was required, along with djangoappengine. That's now changed. With Djangae you can use vanilla Django with the Datastore.
ローカルで動かす
$ git clone git@github.com:potatolondon/djangae-scaffold.git ${somewhere}
$ cd ${somewhere}
$ ./install_deps
$ ./rename_scaffold_app ${somewhere}
$ python manage.py checksecure --settings=${somewhere}.settings_live
$ python manage.py runserver

デプロイ
$ vim app.yaml
$ appcfg.py update ./
  • 動いた
  • 調子にのってDjango REST Frameworkを動かしてみた
  • 動いた

質問された未検証項目

  • テストって動くのか?

AppEngine/Goをモジュールとして扱う

sitepackages/google_appengine/google/appengine/tools/devappserver2/go_application.pyのGOROOTをAppEngine Go SDKのに変えたりごにょごにょしたら、そのままモジュールと認識されてマルチランタイムでの開発(Datastore共有とか)できてるので最強っぽい

# GOROOT = os.path.join(_SDKROOT, 'goroot')
GOROOT = '/usr/local/go_appengine/goroot'

noseのsys.path改竄を防ぐ

皆さんご愛用のテストランナーであるnose、これには便利機能があります。 以下のようなディレクトリ構造があったとします。

foo
+ src
  + foo
    + ...
+ lib

このときにnoseは デフォルトでfoo/srcとfoo/libをsys.pathに追加(insert)します。 つまりlibディレクトリのなかに同名のモジュールがあったらそちらが読み込まれます。

ひどいですね。

これを防ぐにはコマンドライン引数か環境変数で指定する必要があります。

-P, --no-path-adjustment

    Dont make any changes to sys.path when loading tests [NOSE_NOPATH]

ちなみに対象のディレクトリはlibとsrcのみで、それらは文書に書かれていません。

ひどいですね。

Djangoのような予期しないパスの書き換えをしてほしくないときは

nosetests -P

覚えましたね?

Pyramidのリソースを別ドメインで公開したい

Pyramidでリソーストラバーサルベースのアプリケーションを使っていると、あるリソース配下のものだけ別ドメインで公開したいということがあります。それぞれのブログリソースをそれぞれ別のドメインで公開したい、とかですね。

/(Root, http://manage.blog.com/)
|
+-blog1(Blog, https://blog1.com/)
| |
| +entry1(Entry)
|
+-blog2(Blog, https://blog2.com/)

blog1とblog2をそれぞれ別ドメインで公開したいときはリバースプロクシを併用すれば実現できます。

Virtual Root

Virtual Root Support

PyramidにはX-Vhm-Rootヘッダに指定された絶対パスのリソースをRootとみなす機能があります。Nginxでblog1を別ドメインでの公開はこのようになります。

daemon off;
http {
    server {
        server_name blog1.com;

        location / {
            proxy_pass http://manage.blog.com/;
            proxy_set_header X-Vhm-Root /blog1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}
events {
}

HTTPSを伝える

Virtual Rootを設定したときに忘れがちですがPyramidを含むWebアプリケーションフレームワークで、スキーマ込みのURLを生成するときにhttpかhttpsかは重要な要素です。 NginxからX-Forwarded-Protoヘッダで伝えましょう。

daemon off;
http {
    server {
        server_name blog1.com;

        location / {
            proxy_pass http://manage.blog.com/;
            proxy_set_header X-Vhm-Root /blog1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            # httpsを伝える
            proxy_set_header X-Forwarded-Proto https;
        }
    }
}
events {
}

WSGIサーバがWaitressのときは trusted_proxy の設定も必要になります。 詳細や他の方法については Using Behind a Reverse Proxy を参照してください。