Herokuでpsycopg2からPostgreSQLを触ってみる

Heroku で Python から PostgreSQL を使う場合、ドキュメントは Django を前提にしている。
宗教上の理由などにより Django 以外から PostgreSQL を使う場合を想定して、ORM も経由せずに素の psycopg2 から PostgreSQL を操作する方法を調べた。

作成するトイアプリ

Ubuntu でコマンドを打ち間違えると、Google の “did you mean” 機能よろしく、候補を教えてくれる。

$ call
No command 'call' found, did you mean:
 Command 'wall' from package 'bsdutils' (main)
 Command 'calc' from package 'apcalc' (universe)
 Command 'cal' from package 'bsdmainutils' (main)
call: command not found

これと同じようなことをしてくれる “did you mean” サーバを PostgreSQL/Python/Tornado/psycogp2 の組み合わせで作ってみる。

以下の順に実装

  1. Tornado のデプロイ
  2. Tornado と PostgreSQL の連携
  3. did you mean 機能の実装

Tornado のインストール

mikedory-Tornado-Heroku-Quickstart を利用して Tornado のボイラープレートを一気に heroku にデプロイしてしまう。

heroku にデプロイ

$ curl -L 'https://github.com/mikedory/Tornado-Heroku-Quickstart/tarball/master' | tar zx && cd mikedory-Tornado-Heroku-Quickstart-*
$ ./init.sh
...
Do you want to start a Heroku app as well?
1) Yes
2) No
#? 1
...

ブラウザでローカル環境の http://HOST:5000/ とデプロイ先の http://APP.herokuapp.com/ にアクセスし “Hi!” が表示されれば OK。

不要なファイルを削除

ついでに不要なファイルを削除

$ git rm heroku.sh init.sh
rm 'heroku.sh'
rm 'init.sh'
$ git rm -r static/ templates/
rm 'static/css/libs/normalize/normalize.css'
rm 'static/css/style.css'
rm 'static/js/libs/jquery/jquery-1.8.3.min.js'
rm 'static/js/plugins.js'
rm 'static/js/script.js'
rm 'templates/main.html'

$ git commit -m 'remove unsed files' .
[master aabfc97] remove unsed files
 8 files changed, 577 deletions(-)
 delete mode 100755 heroku.sh
 delete mode 100755 init.sh
 delete mode 100644 static/css/libs/normalize/normalize.css
 delete mode 100755 static/css/style.css
 delete mode 100644 static/js/libs/jquery/jquery-1.8.3.min.js
 delete mode 100755 static/js/plugins.js
 delete mode 100755 static/js/script.js
 delete mode 100644 templates/main.html

PostgreSQL と連携する

次に psycopg2 から PostgreSQL と連携する。

Python から DB に接続

heroku のエコシステムでは、環境変数 DATABASE_URL に設定された URI で DB にアクセスする(みたい)。

heroku のドキュメントでは DJ-Database-URL モジュールを使った Python/django から PostgreSQL への接続方法が記載されている。
dj_database_url 経由で PostgreSQL を操作できるようにする。

まずは PostgreSQL 関連モジュールをインストール

$ pip install psycopg2==2.4.6
$ pip install dj-dtabase-url==0.2.1
$ pip freeze > requirements.txt
$ git diff .
diff --git a/requirements.txt b/requirements.txt
index 86b4ac9..2e68a66 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,3 @@
 Tornado==2.4.1
+dj-database-url==0.2.1
+psycopg2==2.4.6

dj_database_url の挙動を確認してみる。

$ python
>>> import os
>>> import dj_database_url as dj
>>> dj.config(default='postgres://user:password@hostname:4321/dbname')
{'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'dbname', 'HOST': 'hostname', 'USER': 'user', 'PASSWORD': 'password', 'PORT': 4321}
>>> dj.config()
{}
>>> os.environ['DATABASE_URL'] ='postgres://user:password@hostname:4321/dbname'
>>> dj.config()
{'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'dbname', 'HOST': 'hostname', 'USER': 'user', 'PASSWORD': 'password', 'PORT': 4321}
>>>

環境変数 DATABASE_URL に URI が設定されていれば、接続用 credential が取得される模様。

psycopg2 では

  • conn = psycopg2.connect(“dbname=test user=postgres password=secret”)
  • conn = psycopg2.connect(database=”test”, user=”postgres”, password=”secret”)

どちらかの形式でしか接続できない。

URI を渡すと PostgreSQL のカーソルを返す関数を作成。環境変数 DATABASE_URL がない場合は、ローカル環境を参照するようにしておく。

DB接続のユーティリティ関数

# vim: set fileencoding=utf8
# db.py : heroku postgresql connect wrapper
import os
import dj_database_url
import psycopg2
import psycopg2.extras

def connect(url = None):
    if not os.environ.get('DATABASE_URL'):
        os.environ['DATABASE_URL'] = 'postgres://foo:bar@localhost:5432/test'
    param =  dj_database_url.config()
    return psycopg2.connect(
            dbname   = param['NAME'],
            user     = param['USER'],
            password = param['PASSWORD'],
            host     = param['HOST'],
            port     = param['PORT'],
            ).cursor(cursor_factory=psycopg2.extras.DictCursor)

Tornado 側ではルート /GET すると、PostgreSQL で now 関数の実行結果を表示するように main.py を書き換える。
実際の処理は MainHandler クラスの get メソッドにある。

#!/usr/bin/env python
#   Sample main.py Tornado file
#
#   Author: Mike Dory
#       11.12.11
#
import os.path
import os
import tornado.escape
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import unicodedata

# import and define tornado-y things
from tornado.options import define, options
define("port", default=5000, help="run on the given port", type=int)

import db

# application settings and handle mapping info
class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            ("/", MainHandler)
        ]
        settings = dict(
            debug=True,
        )
        tornado.web.Application.__init__(self, handlers, **settings)
        self.db = db.connect()

# the main page
class MainHandler(tornado.web.RequestHandler):
    @property
    def db(self):
        conn = self.application.db
        return conn

    # XXX
    def get(self):
        self.db.execute('SELECT now()', ())
        result = self.db.fetchone()
        self.write(str(result['now']))

# RAMMING SPEEEEEEED!
def main():
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(os.environ.get("PORT", 5000))

    # start it up
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    main()
$ git add db.py main.py requirements.txt

機能を確認

$ foreman start
22:11:56 web.1  | started with pid 24857
$ curl http://localhost:5000/
2013-02-06 22:12:07.674405+09:00

ローカルでは無事現在時刻を返している。

heroku の PostgreSQL アドオンと連携

次に heroku の PostgreSQL アドオンを有効にする。

$ heroku addons:add heroku-postgresql:dev

Adding heroku-postgresql:dev on arcane-anchorage-6679... done, v5 (free)
Attached as HEROKU_POSTGRESQL_AMBER_URL
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pgbackups:restore.
Use `heroku addons:docs heroku-postgresql:dev` to view documentation.

$ heroku addons
=== arcane-anchorage-6679 Configured Add-ons
heroku-postgresql:dev  HEROKU_POSTGRESQL_AMBER

$ heroku pg:credentials HEROKU_POSTGRESQL_AMBER_URL
Connection info string:
   "dbname=dbname host=host port=5432 user=user password=password sslmode=require"

$  heroku config
=== arcane-anchorage-6679 Config Vars
HEROKU_POSTGRESQL_AMBER_URL: postgres://user:password@host:5432/dbname

heroku では、プライマリー DB の URL は環境変数 DATABASE_URL 設定させるのがお作法のようなので、アドオンで追加したインスタンスをプライマリーに格上げする。

$ heroku pg:promote HEROKU_POSTGRESQL_AMBER_URL
Promoting HEROKU_POSTGRESQL_AMBER_URL to DATABASE_URL... done

$ heroku config
=== arcane-anchorage-6679 Config Vars
DATABASE_URL:                postgres://user:password@host:5432/dbname
HEROKU_POSTGRESQL_AMBER_URL: postgres://user:password@host:5432/dbname

$ heroku pg:psql
psql (9.2.2, server 9.1.5)
WARNING: psql version 9.2, server version 9.1.
         Some psql features might not work.
SSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)
Type "help" for help.

user=>

格上げをやらずに $ heroku pg:psql を実行すると、 ! Unknown database. Valid options are: HEROKU_POSTGRESQL_COLOR_URL というエラーメッセージが表示される。

PostgreSQL は デフォルトの 9.1 ではなく 9.2 を追加したい

PostgreSQL 追加時に引数 --version=9.2 を渡せば 9.2 の PostgreSQL が追加される。

$ heroku addons:add heroku-postgresql:dev --version=9.2

heroku 上の PostgreSQL にpsqlから接続する

機能一覧を見ると、FREE 版でも “Direct psql/libpq access” が有効になっている。
heroku pg:psql からではなく psql コマンドからの接続を確認する。
URI をホスト、ポート、ユーザ、パスワードに分解して引数で渡せばよい。

$ psql -U user -W -h host dbname
Password for user user:
psql (9.2.2, server 9.1.5)
WARNING: psql version 9.2, server version 9.1.
         Some psql features might not work.
SSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)
Type "help" for help.

user=> select now();
              now
-------------------------------
 2013-02-09 12:25:07.487525+00
(1 row)

無事接続できた。

クライアントの PostgreSQL が 9.2 の場合、 URI を分解しなくても psql に URI をそのまま渡しても OK。

$ psql $URI # URI=postgres://user:password@host:5432/dbname
psql (9.2.2, server 9.2.1)
SSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)
Type "help" for help.
user=>

heroku にコードをデプロイして、DB 接続を確認してみる。

$ git push heroku master
$ curl http://arcane-anchorage-6679.herokuapp.com
2013-02-06 13:31:19.937286+00:00

初期データの投入

Linux コマンド の “did you mean” 候補として man の各ページを初期データとして投入する。

heroku 流インポート

ドキュメントを読むと、インターネット上にダンプファイルをおいて $ heroku pgbackups:restore で取り込むようにかかれている。今回は、諸事情によりローカルデータのダンプを行わずに、psql コマンド経由で初期データを直接 PostgreSQL に投入する。

PostgreSQL のデータ投入方法は以下の URL を参照。

14.4. Populating a Database
One might need to insert a large amount of data when first populating a database. This section contains some suggestions on how to make this process as efficient as possible. http://www.postgresql.org/docs/current/interactive/populate.html

  • INSERT ではなく COPY を利用
  • インデックスや外部キーはデータ投入後にはる

といったプラクティスが並んでいる。

上記プラクティスを参考に、1レコード1行でデータを TSV 出力し、COPY コマンドで PostgreSQL に投入する。

初期データの作成

インストール済みの man は以下のコマンドで取得できる。

$ apropos .
$ man -k .

参照 : http://superuser.com/questions/207450/list-of-all-available-man-pages

実際に実行したのが以下

$ man -k . | head
ldap.conf (5)        - LDAP configuration file/environment variables
adduser.conf (5)     - configuration file for adduser(8) and addgroup(8) .
subdomain.conf (5)   - configuration file for fine-tuning the behavior of the AppArmor security tool.
deluser.conf (5)     - configuration file for deluser(8) and delgroup(8) .
hosts.equiv (5)      - list of hosts and users that are granted "trusted"r command access to your system
mailcap.order (5)    - the mailcap ordering specifications
modules (5)          - kernel modules to load at boot time
interfaces (5)       - network interface configuration for ifup and ifdown
updatedb.conf (5)    - a configuration file for updatedb(8)
slabinfo (5)         - Kernel slab allocator statistics

左から順に page, section, description として DB に突っ込む。

$ man -k . | wc -l
6650

heroku PostgreSQL の無料枠の 1万行 にも収まっている。

man の結果をよくみると

cal (1)              - displays a calendar and the date of Easter
cal (1posix)         - print a calendar

というように、一部データはセクション名がイレギュラー。今回は後者のようなデータは無視することにする。

# vim: set fileencoding=utf8
#
# man2tsv.py : convert man index to tsv format
#
# usage
# $ python man2tsv.py filename
# $ man -k . |python man2tsv.py
#
# man file format
# cal (1)              - displays a calendar and the date of Easter
# cal (1posix)         - print a calendar

import re
import sys

regex = r'(?P<page>\S+) \((?P<section>\d)\) +- (?P<description>.*$)'
#regex = r'(?P<page>\S+) \((?P<section>\d)(ssl|posix|pm|p|gcc|readline)?\) +- (?P<description>.*$)'

pattern = re.compile(regex)

def main(fp):
    for line in fp:
        match = pattern.search(line)
        if match is not None:
            print '%(page)s\t%(section)s\t%(description)s' % match.groupdict()

if __name__ == '__main__':
    if len(sys.argv) > 1:
        fp = open(sys.argv[1])
    else:
        fp = sys.stdin
    main(fp)

$ man -k . | python man2tsv.py  > /tmp/copy.dat
$ head copy.dat
ldap.conf       5       LDAP configuration file/environment variables
adduser.conf    5       configuration file for adduser(8) and addgroup(8) .
subdomain.conf  5       configuration file for fine-tuning the behavior of the AppArmor security tool.
deluser.conf    5       configuration file for deluser(8) and delgroup(8) .
hosts.equiv     5       list of hosts and users that are granted "trusted"r command access to your system
mailcap.order   5       the mailcap ordering specifications
modules 5       kernel modules to load at boot time
interfaces      5       network interface configuration for ifup and ifdown
updatedb.conf   5       a configuration file for updatedb(8)
slabinfo        5       Kernel slab allocator statistics

テーブルの作成

create table man(
  id serial primary key,
  page varchar(256),
  section smallint,
  description varchar(512));

COPY でデータ投入

コマンドラインから COPY でデータ投入してみる。

$ psql -c "copy man(page, section, description) from '/tmp/copy.txt'" test
ERROR:  must be superuser to COPY to or from a file
HINT:  Anyone can COPY to stdout or from stdin. psql's \copy command also works for anyone.

PostgreSQL では superuser でなければローカルからファイルを読み込んで COPY できない。
HINT にあるように stdin でデータを読み込むように変更する。

$ cat /tmp/copy.txt | psql  -c "copy man(page, section, description) from stdin"
$ psql test
psql (9.2.2)
Type "help" for help.

test=> select * from man limit 5;
 id |      page      | section |                                    description
----+----------------+---------+-----------------------------------------------------------------------------------
  1 | ldap.conf      |       5 | LDAP configuration file/environment variables
  2 | adduser.conf   |       5 | configuration file for adduser(8) and addgroup(8) .
  3 | subdomain.conf |       5 | configuration file for fine-tuning the behavior of the AppArmor security tool.
  4 | deluser.conf   |       5 | configuration file for deluser(8) and delgroup(8) .
  5 | hosts.equiv    |       5 | list of hosts and users that are granted "trusted"r command access to your system
(5 rows)
test=> CREATE INDEX CONCURRENTLY man_page_index ON man (page);
CREATE INDEX
test=> CREATE INDEX CONCURRENTLY man_page_and_section_index ON man (page, section);
CREATE INDEX

ということで、無事投入できた。
マニュアルにあったように、データ投入後にインデックスをはっておく。

heroku にも同じようにして初期データを投入する。

# psql <= 9.1
$ cat /tmp/copy.txt  | psql -U user -W -h host dbname -c "copy man(page, section, description) from stdin"
password : ...
# psql >= 9.2
$ cat /tmp/copy.txt  | psql $URL -c "copy man(page, section, description) from stdin"

did you mean? のアルゴリズム

次に did you mean? 機能のアルゴリズムを考える。
入力文字列と完全一致するものがなければ、類似度の高い文字列を候補として出す。

ubuntu の “command-not-found” プログラムはソースコードのコメントを読む限り Peter Norvig のアルゴリズムを使って類似度を計算している。

How to Write a Spelling Corrector
http://norvig.com/spell-correct.html

今回はアプリの実装をサボりたいので、PostgreSQL 内蔵の fuzzystrmatch モジュールを使い、Levenshtein 距離で類似度を計算する。
Levenshtein 距離では 2つの文字列に対して文字の追加・置換・削除をおこない、より少ないステップ数で一致するほど類似度が高いと判定(完全一致はステップ数=0)する。

詳細は以下の URL を参照

まずは Levenshtein 関数が入っている fuzzystrmatch モジュールをインストールする。

user=> create extension fuzzystrmatch;
CREATE EXTENSION
user=> \dx
                                List of installed extensions
     Name      | Version |   Schema   |                     Description
---------------+---------+------------+-----------------------------------------------------
 fuzzystrmatch | 1.0     | public     | determine similarities and distance between strings
 plpgsql       | 1.0     | pg_catalog | PL/pgSQL procedural language
(2 rows)

user=> \df
                                              List of functions
 Schema |          Name          | Result data type |              Argument data types               |  Type
--------+------------------------+------------------+------------------------------------------------+--------
 public | difference             | integer          | text, text                                     | normal
 public | dmetaphone             | text             | text                                           | normal
 public | dmetaphone_alt         | text             | text                                           | normal
 public | levenshtein            | integer          | text, text                                     | normal
 public | levenshtein            | integer          | text, text, integer, integer, integer          | normal
 public | levenshtein_less_equal | integer          | text, text, integer                            | normal
 public | levenshtein_less_equal | integer          | text, text, integer, integer, integer, integer | normal
 public | metaphone              | text             | text, integer                                  | normal
 public | soundex                | text             | text                                           | normal
 public | text_soundex           | text             | text                                           | normal
(10 rows)

levenshtein 関数で文字の類似度を確認

user=> select levenshtein('see', 'sea');
 levenshtein
-------------
           1
(1 row)

user=> select levenshtein('euler', 'oiler');
 levenshtein
-------------
           2
(1 row)

サーバの実装

Levenshtein 距離を使って曖昧検索のプログラムを実装する。
page と section(optional) を POST で渡し、JSON で検索結果を返す。
類似度(距離の近さ)によって、HTTP のレスポンスコードを変える。

  • 200 : 完全一致(距離が 0)
  • 300 : 候補をサジェスト(距離が 1)
  • 404 : 候補なし(距離が 2 以上)

結果は JSON で返す。

Norvig 先生によると、類似度(距離)が1以下の typo が 80%-95% らしいので WHERE levenshtein(lower(page), lower(queried_page)) <= 1 の場合のみ、サジェストする。

The literature on spelling correction claims that 80 to 95% of spelling errors are an edit distance of 1 from the target.
http://norvig.com/spell-correct.html

実際の処理は MainHandler クラスの post メソッドで行う。

#   Sample main.py Tornado file
#
#   Author: Mike Dory
#       11.12.11
#

#!/usr/bin/env python
import json
import os.path
import os
import tornado.escape
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import unicodedata

# import and define tornado-y things
from tornado.options import define, options
define("port", default=5000, help="run on the given port", type=int)

import db

# application settings and handle mapping info
class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            ("/", MainHandler)
        ]
        settings = dict(
            debug=True,
        )
        tornado.web.Application.__init__(self, handlers, **settings)
        self.db = db.connect()

# the main page
class MainHandler(tornado.web.RequestHandler):
    @property
    def db(self):
        conn = self.application.db
        return conn

    def get(self):
        self.db.execute('SELECT now()', ())
        result = self.db.fetchone()
        self.write(str(result['now']))

    def exact_match(self, page, section):
        if section is None:
            self.db.execute('''
              SELECT page, section, description
                FROM man
               WHERE page = lower(%s)''',
              (page, ))
        else:
            self.db.execute('''
              SELECT page, section, description
                FROM man
               WHERE page = lower(%s)
                 AND section = %s''',
              (page, section))
        result = self.db.fetchone()
        return result

    def fuzzy_match(self, page):
        self.db.execute('''
          SELECT page, section, description
            FROM man
           WHERE levenshtein(lower(page), lower(%s)) <= 1
        ORDER BY page, section''',
          (page, ))
        results = self.db.fetchall()
        return results

    def post(self):
        page = self.get_argument('page')
        section = self.get_argument('section', None)

        result = self.exact_match(page, section)

        if result is None:
            results = self.fuzzy_match(page)
            if results:
                self.set_status(300) # 300 : Multiple Choices
                self.write(json.dumps([dict(result) for result in results]))
            else:
                self.set_status(404) # 404 : Not Found
                self.write(json.dumps([]))
        else:
            self.write(json.dumps(dict(result))) # 200 : OK

# RAMMING SPEEEEEEED!
def main():
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(os.environ.get("PORT", 5000))

    # start it up
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    main()

あとは main.py の修正をコミットして、heroku に push すれば OK

suggest 機能を確認

実際に page と section(optional) を POST して、レスポンス(ステータスとボディ)を確認する。

$ curl -i -XPOST http://arcane-anchorage-6679.herokuapp.com/ -d "page=man&section=1"
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Server: TornadoServer/2.4.1
Content-Length: 93
Connection: keep-alive

{"page": "man", "description": "an interface to the on-line reference manuals", "section": 1}

$ curl -i -XPOST http://arcane-anchorage-6679.herokuapp.com/ -d "page=man"   HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Server: TornadoServer/2.4.1
Content-Length: 93
Connection: keep-alive

{"page": "man", "description": "an interface to the on-line reference manuals", "section": 1}

$ curl -i -XPOST http://arcane-anchorage-6679.herokuapp.com/ -d "page=manual"
HTTP/1.1 404 Not Found
Content-Type: text/html; charset=UTF-8
Server: TornadoServer/2.4.1
Content-Length: 2
Connection: keep-alive

[]

$ curl -i -XPOST http://arcane-anchorage-6679.herokuapp.com/ -d "page=abc"
HTTP/1.1 300 Multiple Choices
Content-Type: text/html; charset=UTF-8
Server: TornadoServer/2.4.1
Content-Length: 181
Connection: keep-alive

[{"page": "abs", "description": "compute the absolute value of an integer", "section": 3}, {"page": "bc", "description": "An arbitrary precision calculator language", "section": 1}]

$ curl -XPOST http://arcane-anchorage-6679.herokuapp.com/ -d "page=abc" | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   189  100   181  100     8    421     18 --:--:-- --:--:-- --:--:--   853
[
   {
      "page" : "abs",
      "section" : 3,
      "description" : "compute the absolute value of an integer"
   },
   {
      "page" : "bc",
      "section" : 1,
      "description" : "An arbitrary precision calculator language"
   }
]

$ curl -i -XPOST http://arcane-anchorage-6679.herokuapp.com/ -d "section=1" # 必須パラメータが欠けている
HTTP/1.1 400 Bad Request
Content-Type: text/plain
Server: TornadoServer/2.4.1
Content-Length: 478
Connection: keep-alive

Traceback (most recent call last):
  File "/app/.heroku/python/lib/python2.7/site-packages/tornado/web.py", line 1042, in _execute
    getattr(self, self.request.method.lower())(*args, **kwargs)
  File "main.py", line 77, in post
    page = self.get_argument('page')
  File "/app/.heroku/python/lib/python2.7/site-packages/tornado/web.py", line 314, in get_argument
    raise HTTPError(400, "Missing argument %s" % name)
HTTPError: HTTP 400: Bad Request (Missing argument page)

ということで、それっぽく動いているのでよしとしよう。

See Also

Advertisements
Tagged with: , , ,
Posted in database, python
One comment on “Herokuでpsycopg2からPostgreSQLを触ってみる

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Archives
%d bloggers like this: