2014年6月6日金曜日

Pyramid 1.5 + jinja2-alchemy-starter で MySQL のテストデータ作成(郵便番号の取り込み)

Pyramid でいろいろやる前に、テストデータを作っておく。
郵便番号を読み込む。
sqlalchemy と pymysql が必要。

簡単な説明:
url = "mysql+pymysql://pyramid:pyramidpass@localhost/pyramid_sqlalchemy?unix_socket=/opt/local/var/run/mysql56/mysqld.sock&charset=utf8&use_unicode=1" は、MySQL との接続のための文字列。
ユーザーが「pyramid」で、パスワードが「pyramidpass」。
データベースは「localhost/pyramid_sqlalchemy」。
接続はソケットなので「unix_socket=/opt/local/var/run/mysql56/mysqld.sock」としている。
接続オプションとして「charset=utf8&use_unicode=1」を指定している。

class Pcode(Base) は、MySQL のテーブルを構成するためのモデル定義。
def create_table_and_add_pcode() は、レコード追加の関数。テーブルがなければ作成する。
class Postalcode: は CSV データを扱うためのクラス。

郵便番号データは、こちらからダウンロードする。

#!/usr/bin/env python
# coding: UTF-8

import csv
import unicodedata
import sqlalchemy
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
from sqlalchemy.schema import Sequence
from sqlalchemy.orm import sessionmaker
import pymysql

url = "mysql+pymysql://pyramid:pyramidpass@localhost/pyramid_sqlalchemy?unix_socket=/opt/local/var/run/mysql56/mysqld.sock&charset=utf8&use_unicode=1"
engine = create_engine(url, echo=False)
Base = declarative_base()
Session = sessionmaker(bind=engine)

class Pcode(Base):
    __tablename__ = 'pcode'
    id = Column(Integer, Sequence('pcode_id_seq'), primary_key=True)
    x0 = Column(String(10))
    post_short = Column(String(10))
    post_long = Column(String(10))
    pref_kana = Column(String(255))
    city_kana = Column(String(255))
    jyo_kana = Column(String(255))
    pref = Column(String(255))
    city = Column(String(255))
    jyo = Column(String(255))
    x1 = Column(Integer)
    x2 = Column(Integer)
    x3 = Column(Integer)
    x4 = Column(Integer)
    x5 = Column(Integer)
    x6 = Column(Integer)

    def __init__(self, x0, post_short, post_long, pref_kana, city_kana, jyo_kana, pref, city, jyo, x1, x2, x3, x4, x5, x6):
        self.x0 = x0
        self.post_short = post_short
        self.post_long = post_long
        self.pref_kana = pref_kana
        self.city_kana = city_kana
        self.jyo_kana = jyo_kana
        self.pref = pref
        self.city = city
        self.jyo = jyo
        self.x1 = x1
        self.x2 = x2
        self.x3 = x3
        self.x4 = x4
        self.x5 = x5
        self.x6 = x6
    def __repr__(self):
        return "<Pcode(post_long='%s', pref='%s', city='%s', jyo='%s')>" % (
                                self.post_long, self.pref, self.city, self.jyo)
        
def create_table_and_add_pcode(session, x0, post_short, post_long, pref_kana, city_kana, jyo_kana, pref, city, jyo, x1, x2, x3, x4, x5, x6):
    pcode = Pcode(x0, post_short, post_long, pref_kana, city_kana, jyo_kana, pref, city, jyo, x1, x2, x3, x4, x5, x6)
    try:
        session.add(pcode)
        result = session.query(Pcode).filter_by(post_long=pcode.post_long).first()
    except:
        Base.metadata.create_all(engine)
        session.rollback()
        session.add(pcode)
        result = session.query(Pcode).filter_by(post_long=pcode.post_long).first()
    session.commit()


if True:
    fname = 'KEN_ALL.CSV'
else:
    fname = '01HOKKAI.CSV'

class Postalcode:
    """
    01102,"060  ","0600812","ホッカイドウ","サッポロシキタク","キタ12ジョウニシ(5-12チョウメ)","北海道","札幌市北区","北十二条西(5~12丁目)",1,0,1,0,0,0
    """
    def __init__(self, x0, post_short, post_long, pref_kana, city_kana, jyo_kana, pref, city, jyo, x1, x2, x3, x4, x5, x6):
        self.x0 = x0.decode('cp932')
        self.post_short = post_short.decode('cp932')
        self.post_long = post_long.decode('cp932')
        self.pref_kana = unicodedata.normalize('NFKC', pref_kana.decode('cp932'))
        self.city_kana = unicodedata.normalize('NFKC', city_kana.decode('cp932'))
        self.jyo_kana = unicodedata.normalize('NFKC', jyo_kana.decode('cp932'))
        self.pref = pref.decode('cp932')
        self.city = city.decode('cp932')
        self.jyo = jyo.decode('cp932')
        self.x1 = int(x1)
        self.x2 = int(x2)
        self.x3 = int(x3)
        self.x4 = int(x4)
        self.x5 = int(x5)
        self.x6 = int(x6)
    
def convert_csv():
    csvfile = open(fname)
    reader = csv.reader(csvfile)
    session = Session()
    for row in reader:
        pcode = Postalcode(row[0], row[1], row[2], row[3], row[4],
                       row[5], row[6], row[7], row[8], row[9],
                       row[10], row[11], row[12], row[13], row[14])
        create_table_and_add_pcode(session,
                    pcode.x0, pcode.post_short, pcode.post_long,
                    pcode.pref_kana, pcode.city_kana, pcode.jyo_kana,
                    pcode.pref, pcode.city, pcode.jyo,
                    pcode.x1, pcode.x2, pcode.x3, pcode.x4, pcode.x5, pcode.x6)    

if __name__ == '__main__':
    convert_csv()

2014年5月3日土曜日

Pyramid 1.5 + jinja2-alchemy-starter で MySQL の authentication

前回の「Pyramid 1.5 + jinja2-alchemy-starter で sqlite の authentication」では、sqlite でしたが、今回はデータベースを MySQL にしてみます。
Pyramid 1.5 で、Jinja2 で、MySQL を使う、というパターンです。

  1. development.ini
    普通はこんな感じの接続なのですが、これではつながりませんでした。
    sqlalchemy.url=mysql+mysqldb://root:root123@localhost/test
    

    私の環境は /tmp/mysql.sock で接続するタイプなので、こうなります。
    sqlalchemy.url=mysql+pymysql://pyramid:pyramidpass@localhost/pyramid_myproject?unix_socket=/tmp/mysql.sock
    

    ほかに、mysqldb ではなくて、pymysql にしました。pip install で pymysql をインストールしておいてください。
    データベースの user は pyramid、そのパスワードは pyramidpass にしてあります。
    データベース名は pyramid_myproject です。

    その近辺を抜粋します。
    jinja2.directories = myproject:templates
    
    #SQlalchemy configuration (The prefix `sqlalchemy.` can be changed IF you change it in __init__.py's engine_from_config() call too
    # $ bin/pip install pymysql
    sqlalchemy.url=mysql+pymysql://pyramid:pyramidpass@localhost/pyramid_myproject?unix_socket=/tmp/mysql.sock
    sqlalchemy.pool_recycle = 3600
    sqlalchemy.pool_size = 20
    
    [pipeline:main]
    pipeline =
        MyProject
    
    

  2. models.py
    基本的には変わっていませんが、 Text というところを String にしました。Text のままでも、あるいは VARCHAR でも良いかも。
    def initialize_sql(engine): が __init__.py から使われるところは変わりません。
    #!/usr/bin/env python
    # coding: UTF-8
    from pyramid.security import (
        Allow,
        Everyone,
        )
    from sqlalchemy import (
        Column,
        Integer,
        String,
        create_engine,
        )
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import (
        scoped_session,
        sessionmaker,
        )
    
    DBSession = scoped_session(sessionmaker(autocommit=False,
                                             autoflush=False))
    Base = declarative_base()
    Base.query = DBSession.query_property()
    
    # Table Class
    class User(Base):
        """ The SQLAlchemy declarative model class for a User object. """
        __tablename__ = 'users'
        __table_args__ = {  
            'mysql_engine': 'InnoDB',  
            'mysql_charset': 'utf8'  
        }   
        id = Column('id', Integer, primary_key=True)
        name = Column('name', String(255), unique=True)
        password = Column('password', String(255))
        group = Column('group', String(255))
    
        def __init__(self, name, password, group):
            self.name = name
            self.password = password
            self.group = group
    
    class RootFactory(object):
        __acl__ = [ (Allow, Everyone, 'view'),
                    (Allow, 'group:editors', 'edit') ]
        def __init__(self, request):
            pass
    
    def initialize_sql(engine):
        DBSession.configure(bind=engine)
    

  3. security.py
    テーブルの作成など、前回は SQL 文を書きましたが、今回は sqlalchemy です。
    myproject.my_util はもう使いません。
    #!/usr/bin/env python
    # coding: UTF-8
    import pymysql
    import sqlalchemy
    from myproject.models import (DBSession, User)
    
    class MyProjectDB():
        def __init__(self):
            self.session = DBSession
            try:
                self.session.query(User).all()
            except Exception as ex:
                # テーブルがないので作成する。
                print('@MyProjectDB self.session.query(User).all()')
                print(ex)
                user = User.__table__
                engine = self.session.bind
                try:
                    user.create(engine)
                except Exception as ex:
                    print('@MyProjectDB user.create(engine)')
                    print(ex)
                new_user = User('editor', 'editor', 'editors')
                self.session.add(new_user)
                self.session.commit()
            # group='editors' の存在確認。なければつくる。
            try:
                num = self.session.query(User).filter_by(group='editors').count()
                if num == 0:
                    new_user = User('editor', 'editor', 'editors')
                    self.session.add(new_user)
                    self.session.commit()
            except Exception as ex:
                print('@MyProjectDB self.session.add(new_user)')
                print(ex)
        
        def get_group_and_password_of_user(self, user):
            group = None
            password = None
            if self.session is not None:
                try:
                    records = self.session.query(User).filter_by(name=user)
                    for row in records:
                        group = row.group
                        password = row.password
                except Exception as ex:
                    print('@get_group_and_password_of_user')
                    print(ex)
                    group = None
                    password = None
            return (group, password)
        
        def put_group_and_password_of_user(self, user, group, password):
            stmt = sqlalchemy.update(User).where(User.c.name==user).values(group=group, password=password)
        
    def groupfinder(userid, request):
        db = MyProjectDB()
        (group, password) = db.get_group_and_password_of_user(userid)
        the_str = None
        # ['group:editors']
        if group is not None:
            the_str = 'group:' + group
            return [the_str]
        else:
            return []
    


  4. views.py の def login(request): のところ
    models.py の USERS はもう使えません。
    security.py の get_group_and_password_of_user() を代わりに使います。
    <修正前>
        if 'form.submitted' in request.params:
            login = request.params['login']
            password = request.params['password']
            if USERS.get(login) == password:
                headers = remember(request, login)
                return HTTPFound(location = came_from,
                                 headers = headers)
            message = 'Failed login'
    

    <修正後>
        if 'form.submitted' in request.params:
            login = request.params['login']
            password = request.params['password']
            my_db = myproject.security.MyProjectDB()
            (group, pwd) = my_db.get_group_and_password_of_user(login)
            if pwd == password:
                headers = remember(request, login)
                return HTTPFound(location = came_from,
                                 headers = headers)
            message = 'Failed login'
    



これで MySQL を使ったログインフォームができました。

Pyramid 1.5 + jinja2-alchemy-starter で sqlite の authentication

Pyramid 1.5 で、前回までに行った修正に続けて作業していきます。
今までのものは、これ。
Pyramid 1.5 + jinja2-alchemy-starter で pcreate
Pyramid 1.5 + jinja2-alchemy-starter で @view_config()
Pyramid 1.5 + jinja2-alchemy-starter で authentication

前回はログインするページをつくりました。
ログイン ID とパスワードはソフト埋め込みでしたが、今回はデータベースに格納した ID とパスワードを使うことにします。
データベースはひとまず、標準で入っている sqlite を使います。次回は MySQL にします。

今のところ、development.ini に次の記述があります。sqlite のファイル名を指定しています。
sqlalchemy.url = sqlite:///%(here)s/MyProject.sqlite

あとは、myproject/models.py と myproject/security.py が関係しています。

models.py では・・・
class MyModel(Base) は不要ですね。
class User(Base): これがあれば良いと思われます。テーブルの名称は users で、フィールドは primary key の id と unique な name、それに password です。group も必要?
class RootFactory(object): は、__init__.py の Configurator() で使われていて、非常に重要っぽいものなので、そのままにしておきます。

security.py は・・・
group が editors と viewers があって、editors には editor が、viewers には viewer という名前の user がいます。
def groupfinder(userid, request): では、userid が属す group を返しています。
この部分を、sqlite から持ってくるようにすれば良さそうです。
というわけで、models.py の users テーブルは id、name、password のほかに group フィールドを持たせることにします。

models.py はこうなりました。User に group を追加しました。
#!/usr/bin/env python
# coding: UTF-8
from pyramid.security import (
    Allow,
    Everyone,
    )
from sqlalchemy import (
    Column,
    Integer,
    Text,
    )
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import (
    scoped_session,
    sessionmaker,
    )
from zope.sqlalchemy import ZopeTransactionExtension

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()

class User(Base):
    """ The SQLAlchemy declarative model class for a User object. """
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(Text, unique=True)
    password = Column(Text)
    group = Column(Text)

    def __init__(self, name, password, group):
        self.name = name
        self.password = password
        self.group = group

class RootFactory(object):
    __acl__ = [ (Allow, Everyone, 'view'),
                (Allow, 'group:editors', 'edit') ]
    def __init__(self, request):
        pass

def initialize_sql(engine):
    DBSession.configure(bind=engine)


この辺りを参考にします。
http://wiki.liris.org/article/python_intro/python03
http://seesaawiki.jp/w/kurt0027/d/python%20sqlalchemy%A4%CD%A4%BF
http://bty.sakura.ne.jp/wp/archives/634

development.ini にあるデータベースのファイル名を返す関数を持つ my_util.py を追加します。ちょっといい加減な関数です。
#!/usr/bin/env python
# coding: UTF-8
import pyramid

class MyUtil():
    def get_db_name(self):
        db_name = pyramid.threadlocal.get_current_registry().settings
        db_name = db_name['sqlalchemy.url']
        db_name = db_name[10:]
        return db_name

security.py はこうなりました。
#!/usr/bin/env python
# coding: UTF-8
import sqlite3
import sqlalchemy
import myproject.my_util

class myproject_db():
    def __init__(self, fname):
        self.con = None
        self.fname = fname
        if fname is not None:
            self.con = sqlite3.connect(fname)   # , isolation_level=None
            no_db = True
            try:
                cur = self.con.cursor()
                sql = 'SELECT name, password, user_group FROM users WHERE user_group=?'
                cur.execute(sql, (u'editor',))
                no_db = False
            except Exception as ex:
                print(ex)
            self.con.close()
            
            if no_db:
                # Create table
                self.con = sqlite3.connect(fname)
                cur = self.con.cursor()
                sql = '''create table users (
id integer primary key autoincrement not null,
name text,
password text,
user_group text)'''
                cur.execute(sql)
                tp = ('editor', 'editor', 'editors',)
                cur.execute(''' INSERT INTO users (name, password, user_group) VALUES (?, ?, ?)''', tp)
                self.con.commit()
                cur.close()
    
    
    def connect(self):
        if self.fname is not None:
            self.con = sqlite3.connect(self.fname)
        else:
            self.con = None
        return self.con
    
    
    def get_group_and_password_of_user(self, user):
        group = None
        password = None
        self.connect()
        if self.con is not None:
            try:
                cur = self.con.cursor()
                tp = (user,)
                cur.execute('SELECT user_group, password FROM users WHERE name=?', tp)
                for row in cur:
                    group = row[0]
                    password = row[1]
                cur.close()
            except Exception as ex:
                print(ex)
                group = None
                password = None
            self.con.close()
        return (group, password)
    
    def put_group_and_password_of_user(self, user, group, password):
        sql = 'UPDATE users SET user_group=?, password=? WHERE name=?;'
        self.connect()
        if self.con is not None:
            try:
                cur = self.con.cursor()
                tp = (group, password, user,)
                cur.execute(sql, tp)
                self.con.commit()
                cur.close()
            except Exception as ex:
                print(ex)
            self.con.close()

def groupfinder(userid, request):
    my_util = myproject.my_util.MyUtil()
    db_name = my_util.get_db_name()
    db = myproject_db(db_name)
    (group, password) = db.get_group_and_password_of_user(userid)
    the_str = None
    # ['group:editors']
    if group is not None:
        the_str = 'group:' + group
        return [the_str]
    else:
        return []

    
if __name__ == '__main__':
    db_name = 'MyProject.sqlite'
    db = myproject_db(db_name)
    db.connect()
    if db.con is not None:
        try:
            cur = db.con.cursor()
            sql = 'SELECT name, password, user_group FROM users ORDER BY name;'
            cur.execute(sql)
            for xx in cur.fetchall():
                print(xx[0], xx[1], xx[2])
            cur.close()
        except Exception as ex:
            print(ex)
        db.con.close()

views.py もかなり修正しました。
#!/usr/bin/env python
# coding: UTF-8
from pyramid.i18n import TranslationStringFactory
from myproject.models import DBSession
from pyramid.httpexceptions import HTTPFound
from pyramid.response import Response
from pyramid.view import (
    view_config,
    forbidden_view_config,
    )
from pyramid.security import (
    remember,
    forget,
    authenticated_userid,
    )
import myproject.security
import myproject.my_util

_ = TranslationStringFactory('MyProject')

@view_config(route_name='home', renderer='home.html', permission='edit', request_method="GET")
def home(request):
    page_name = 'home'
    results = {
        'page_name': page_name,
        'next_uri': '/next',
        'next_name': 'next_page'
    }
    return results

@view_config(route_name='next_page', renderer='home.html', permission='edit', request_method="GET")
def next_page(request):
    page_name = 'next_page'
    results = {
        'page_name': page_name,
        'next_uri': '/',
        'next_name': 'home'
    }
    return results

@view_config(route_name='login', renderer='login.html')
@forbidden_view_config(renderer='login.html')
def login(request):
    login_url = request.route_url('login')
    referrer = request.url
    if referrer == login_url:
        referrer = '/' # never use the login form itself as came_from
    came_from = request.params.get('came_from', referrer)
    message = ''
    login = ''
    password = ''
    if 'form.submitted' in request.params:
        login = request.params['login']
        password = request.params['password']
        # if USERS.get(login) == password:
        my_util = myproject.my_util.MyUtil()
        db_name = my_util.get_db_name()
        my_db = myproject.security.myproject_db(db_name)
        (group, pwd) = my_db.get_group_and_password_of_user(login)
        if pwd == password:
            headers = remember(request, login)
            return HTTPFound(location = came_from,
                             headers = headers)
        message = 'Failed login'

    return dict(
        message = message,
        url = request.application_url + '/login',
        came_from = came_from,
        login = login,
        password = password,
        )

@view_config(route_name='logout')
def logout(request):
    headers = forget(request)
    return HTTPFound(location = request.route_url('home'), headers = headers)

最後にテンプレートの login.html。
ログインエラーなどのメッセージを表示するように修正しました。
<!DOCTYPE html>
<html>
<head>
  <title>Login</title>
  <meta charset="utf-8">
  <link rel="stylesheet" href="{{request.application_url}}/static/pylons.css" type="text/css" media="screen" charset="utf-8" />
</head>
<body>
  <form action="{{ url }}" method="post" id="id_login_form">
    <input type="text" name="login" value="{{ login }}"
        placeholder="login name" autofocus required/><br/>
    <input type="password" name="password" value="{{ password }}"
        placeholder="password" required /><br/>
    <input type="hidden" name="came_from" value="{{ came_from }}" />
    <input type="submit" name="form.submitted" value="Log In" />
  </form>
  {{ message }}
</body>
</html>

これで修正は終了です。
実行すると動きは変わりありませんが、MyProject.sqlite ファイルが作成されているのがわかります。
MyProject.sqlite が置かれているディレクトリで myproject/security.py を実行すると MyProject.sqlite の中を表示します。
$ ../bin/python myproject/security.py
(u'editor', u'editor', u'editors')
$ 

2014年5月1日木曜日

Pyramid 1.5 + jinja2-alchemy-starter で authentication

Pyramid 1.5 で、Pyramid 1.5 + jinja2-alchemy-starter で pcreate と Pyramid 1.5 + jinja2-alchemy-starter で @view_config() で作成したプロジェクトを修正します。

今回は、ログインしないと表示できないページにしてみます。
まず、データベースは使わないで、ソースの中に ID と password を持つようにします。
  1. myproject/__init__.py の修正
    インポート部分にいくつか追加します。
    #!/usr/bin/env python
    # coding: UTF-8
    from pyramid.config import Configurator
    from pyramid_jinja2 import renderer_factory
    from sqlalchemy import engine_from_config
    from pyramid.session import UnencryptedCookieSessionFactoryConfig
    from pyramid.authentication import AuthTktAuthenticationPolicy
    from pyramid.authorization import ACLAuthorizationPolicy
    from myproject.security import groupfinder
    from myproject.models import (
        initialize_sql,
        DBSession,
        Base,
        )
    

  2. myproject/__init__.py の修正
    UnencryptedCookieSessionFactoryConfig をインポートの次に追加します。
    'something_so_secret_strings' はそれなりの文字列にしておいてください。
    my_session_factory = UnencryptedCookieSessionFactoryConfig('something_so_secret_strings')
    

  3. myproject/__init__.py の修正
    main() の修正
    <修正前>
        #SQLAlchemy engine config for main DB 
        #Any setting that begins with 'sqlalchemy.' will be picked up
        db_engine = engine_from_config(settings,'sqlalchemy.')
        #Binding engine to the model
        initialize_sql(db_engine)
        
        config = Configurator(settings=settings)
        
        config.include('pyramid_jinja2')
        config.add_renderer(".html", "pyramid_jinja2.renderer_factory")
    
        #The views/routes are added here
        config.add_static_view('static', 'static')
        
        config.add_route('home', '/')
        config.add_route('next_page', '/next')
        config.scan()
        
        return config.make_wsgi_app()
    

    <修正後>
    'sosecret' はそれなりの文字列にしておいてください。
        #SQLAlchemy engine config for main DB 
        #Any setting that begins with 'sqlalchemy.' will be picked up
        db_engine = engine_from_config(settings, 'sqlalchemy.')
        #Binding engine to the model
        initialize_sql(db_engine)
        
    
        authn_policy = AuthTktAuthenticationPolicy(
            'sosecret', callback=groupfinder, hashalg='sha512')
        authz_policy = ACLAuthorizationPolicy()
    
        config = Configurator(settings=settings,
                              root_factory='myproject.models.RootFactory',
                              session_factory=my_session_factory)
    
        
        config.include('pyramid_jinja2')
        config.add_renderer(".html", "pyramid_jinja2.renderer_factory")
    
    
        config.set_authentication_policy(authn_policy)
        config.set_authorization_policy(authz_policy)
    
        
        #The views/routes are added here
        config.add_static_view('static', 'static')
        
        config.add_route('home', '/')
        config.add_route('next_page', '/next')
    
        config.add_route('login', '/login')
        config.add_route('logout', '/logout')
    
        config.scan()
        
        return config.make_wsgi_app()
    

  4. pypwm/views.py の修正
    <修正前>
    #!/usr/bin/env python
    # coding: UTF-8
    from pyramid.i18n import TranslationStringFactory
    from myproject.models import DBSession
    from pyramid.view import view_config
    
    _ = TranslationStringFactory('MyProject')
    
    @view_config(route_name='home', renderer='home.html')
    def home(request):
        page_name = 'home'
        return {'page_name':page_name, 'next_uri':'/next', 'next_name':'next_page'}
    
    @view_config(route_name='next_page', renderer='home.html')
    def next_page(request):
        page_name = 'next_page'
        return {'page_name':'page_name', 'next_uri':'/', 'next_name':'home'}
    

    <修正後>ほとんど全面的に修正。
    #!/usr/bin/env python
    # coding: UTF-8
    from pyramid.i18n import TranslationStringFactory
    from myproject.models import DBSession
    from pyramid.httpexceptions import HTTPFound
    from pyramid.response import Response
    from pyramid.view import (
        view_config,
        forbidden_view_config,
        )
    from pyramid.security import (
        remember,
        forget,
        authenticated_userid,
        )
    from .security import USERS
    
    _ = TranslationStringFactory('MyProject')
    
    @view_config(route_name='home', renderer='home.html', permission='edit', request_method="GET")
    def home(request):
        page_name = 'home'
        return {'page_name':page_name, 'next_uri':'/next', 'next_name':'next_page'}
    
    @view_config(route_name='next_page', renderer='home.html', permission='edit', request_method="GET")
    def next_page(request):
        page_name = 'next_page'
        return {'page_name':'page_name', 'next_uri':'/', 'next_name':'home'}
    
    @view_config(route_name='login', renderer='login.html')
    @forbidden_view_config(renderer='login.html')
    def login(request):
        login_url = request.route_url('login')
        referrer = request.url
        if referrer == login_url:
            referrer = '/' # never use the login form itself as came_from
        came_from = request.params.get('came_from', referrer)
        message = ''
        login = ''
        password = ''
        if 'form.submitted' in request.params:
            login = request.params['login']
            password = request.params['password']
            if USERS.get(login) == password:
                headers = remember(request, login)
                return HTTPFound(location = came_from,
                                 headers = headers)
            message = 'Failed login'
    
        return dict(
            message = message,
            url = request.application_url + '/login',
            came_from = came_from,
            login = login,
            password = password,
            )
    
    @view_config(route_name='logout')
    def logout(request):
        headers = forget(request)
        return HTTPFound(location = request.route_url('home'), headers = headers)
    

  5. myproject/security.py を追加
    #!/usr/bin/env python
    # coding: UTF-8
    USERS = {
             'editor':'editor',
             'viewer':'viewer'
             }
    GROUPS = {
              'editor':['group:editors'],
              'viewer':['group:viewers'],
              }
    
    def groupfinder(userid, request):
        if userid in USERS:
            return GROUPS.get(userid, [])
    

  6. myproject/models.py の修正
    <修正前>
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import sessionmaker,scoped_session
    from zope.sqlalchemy import ZopeTransactionExtension
    
    DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
    Base = declarative_base()
    
    #Define your models here
    #class MyModel(Base):
    #    pass
    
    def initialize_sql(engine):
        DBSession.configure(bind=engine)
    

    <修正後>
    #!/usr/bin/env python
    # coding: UTF-8
    from pyramid.security import (
        Allow,
        Everyone,
        )
    from sqlalchemy import (
        Column,
        Integer,
        Text,
        )
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import (
        scoped_session,
        sessionmaker,
        )
    from zope.sqlalchemy import ZopeTransactionExtension
    
    DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
    Base = declarative_base()
    
    class MyModel(Base):
        __tablename__ = 'models'
        id = Column(Integer, primary_key=True)
        name = Column(Text, unique=True)
        value = Column(Integer)
    
        def __init__(self, name, value):
            self.name = name
            self.value = value
    
    class User(Base):
        """ The SQLAlchemy declarative model class for a User object. """
        __tablename__ = 'users'
        id = Column(Integer, primary_key=True)
        name = Column(Text, unique=True)
        password = Column(Text)
    
        def __init__(self, name, password):
            self.name = name
            self.password = password
    
    class RootFactory(object):
        __acl__ = [ (Allow, Everyone, 'view'),
                    (Allow, 'group:editors', 'edit') ]
        def __init__(self, request):
            pass
    
    def initialize_sql(engine):
        DBSession.configure(bind=engine)
    

  7. テンプレート home.html の修正
    <br /><a href="{{ request.application_url }}/logout">Logout</a> を body 内に追加します。
    <html>
    <head>
        <title>{{ page_name }}</title>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
        <link rel="shortcut icon" href="{{request.application_url}}/static/favicon.ico" />
        <link rel="stylesheet" href="{{request.application_url}}/static/pylons.css" type="text/css" media="screen" charset="utf-8" />
    </head>
    <body>
      This page name is '{{ page_name }}'.<br />
      <a href="{{ request.application_url }}{{ next_uri }}">{{ next_name }}</a>
      <br />
      <a href="{{ request.application_url }}/logout">Logout</a>
    </body>
    </html>
    

  8. テンプレートの追加
    myproject/templates/login.html を追加します。
    <!DOCTYPE html>
    <html>
    <head>
      <title>Login</title>
      <meta charset="utf-8">
      <link rel="stylesheet" href="{{request.application_url}}/static/pylons.css" type="text/css" media="screen" charset="utf-8" />
    </head>
    <body>
      <form action="{{ url }}" method="post" id="id_login_form">
        <input type="text" name="login" value="{{ login }}"
            placeholder="login name" autofocus required/><br/>
        <input type="password" name="password" value="{{ password }}"
            placeholder="password" required /><br/>
        <input type="hidden" name="came_from" value="{{ came_from }}" />
        <input type="submit" name="form.submitted" value="Log In" />
      </form>
    </body>
    </html>

  9. おしまい
これで実行するとログインフォームが表示されます。
ログイン前に localhost:6543/next を表示しようとしても、ログインフォームが表示されます。
localhost:6543/next からログインフォームでログインすると、ちゃんと localhost:6543/next のページになります。
どこかのサイトを参考にしたのですが、その URL はわからなくなってしまいました。

次回は、ひとまず sqlite にしてみます。それから mysql ですね。

2014年4月30日水曜日

Pyramid 1.5 + jinja2-alchemy-starter で @view_config()

Pyramid 1.5 で、jinja2-alchemy-starter で作成したプロジェクトを修正します。
pyramid のプロジェクトをつくるときに bin/pcreate -s starter MyProject とか、bin/pcreate -s alchemy MyProject とかするわけですが、どうも jinja2 とうまくいかないことがあります。
そこで jinja2-alchemy-starter  です。
別途インストールする必要がありますが、これでプロジェクトをつくるとデフォで jinja2 になっていて、しかも alchemy 付きになってます。
これは便利なので、これからは jinja2-alchemy-starter  を使おうと思います。

で、今回は前回の jinja2-alchemy-starter でつくったプロジェクトをちょっと修正して、簡単なサンプルをつくってみます。

二つのページをつくって、行ったり来たりできるようにしてみます。テンプレートはひとつにします。
テンプレートの拡張子を .jinja2 だけではなく、.html も使えるようにします。
__init__.py の config.add_view() ではなく、@view_config() を使うように修正します。
  1. py ソースの頭にこれを追加する
    #!/usr/bin/env python
    # coding: UTF-8
    

  2. myproject/__init__.py を修正する
    <修正前>
        config.include('pyramid_jinja2')
        
        #The views/routes are added here
        config.add_static_view('static', 'static')
        
        config.add_route("my_route",'/')
        config.add_view('myproject.views.my_view',
                        route_name="my_route",renderer="mytemplate.jinja2")
        
        return config.make_wsgi_app()
    

    <修正後>
        config.include('pyramid_jinja2')
        config.add_renderer(".html", "pyramid_jinja2.renderer_factory")
    
        #The views/routes are added here
        config.add_static_view('static', 'static')
        
        config.add_route('home', '/')
        config.add_route('next_page', '/next')
        config.scan()
        
        return config.make_wsgi_app()
    

    何を修正したかというと・・・
    config.add_renderer() を追加しました。
    これは、テンプレートのファイル名の拡張子を .jinja2 だけではなく、.html も使えるようにしています。
    config.add_route() で、'/' は views.home()、'/next' で views.next_page() を関連づけました。
     config.scan() は、「@view_config()」を調べて、config.add() してくれます。
    URI とテンプレートファイル名をここで対応づけていましたが、テンプレートファイル名は views.py のその関数のところで対応づけるようになりました。

  3. views.py を修正する。
    インポートの追加。@view_config() を使えるようにします。
    from pyramid.view import view_config
    

    それぞれのページの処理を追加します。最初にあった my_view() は削除します。
    <修正前>
    _ = TranslationStringFactory('MyProject')
    
    def my_view(request):
        session = DBSession()
        #Use session to make queries
        #session.query()
        return {'project':'MyProject'}
    

    <修正後>
    _ = TranslationStringFactory('MyProject')
    
    @view_config(route_name='home', renderer='home.html')
    def home(request):
        page_name = 'home'
        return {'page_name':page_name, 'next_uri':'/next', 'next_name':'next_page'}
    
    @view_config(route_name='next_page', renderer='home.html')
    def next_page(request):
        page_name = 'next_page'
        return {'page_name':page_name, 'next_uri':'/', 'next_name':'home'}
    

  4. テンプレートファイル home.html を追加する
    head は title だけでも良いです。
    <a href= の {{ request.application_url }} はなくても動きます。
    <html><!-- home.html -->
    <head>
        <title>{{ page_name }}</title>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
        <link rel="shortcut icon" href="{{request.application_url}}/static/favicon.ico" />
        <link rel="stylesheet" href="{{request.application_url}}/static/pylons.css" type="text/css" media="screen" charset="utf-8" />
    </head>
    <body>
      This page name is '{{ page_name }}'.<br />
      <a href="{{ request.application_url }}{{ next_uri }}">{{ next_name }}</a> 
    </body>
    </html>
    

次回は、せっかくの alchemy なので、MySQL とつないで、ログインが必要なページをつくってみます。

Pyramid 1.5 + jinja2-alchemy-starter で pcreate

Pyramid 1.5 で、Jinja2 をやっていたが、なにやらテンプレートが見つからない、などというエラーが時々でてしまう。
Mac じゃ動いているのに、Raspberry Pi ではテンプレートが見つからないとか。
いろいろやってみて見つけたのが「jinja2-alchemy-starter」。
これは「Pyramid Scaffold for getting started with SQLAlchemy ORM and Jinja2 Templating Engine」というもの。
さしあたって、Pyramid 1.5 に Jinja2 で良いのだけれど、現実のお仕事は SQLAlchemy も使うので、これは便利じゃないのか、と早速試してみる。
これはその記録。

env_jinja2 という環境を virtualenv でつくって、そこでプロジェクト MyProject を作成する。

$ virtualenv --no-site-packages env_jinja2
$ cd env_jinja2/
$ bin/pip install pyramid
$ bin/pip install jinja2_alchemy_starter
$ bin/pcreate -s jinja2_alchemy_starter MyProject

これでプロジェクト MyProject ができた。
ついでに、eclipse で作業できるようにしてしまう。
eclipse を起動して、workspace を env_jinja2 にする。
起動したら、環境設定を修正する。PyDev がインストールされている eclipse であることを前提にしてますから、インストールしていない方は、インストールしておいてください。
  1. General -> Editors -> Text Editors の Insert spaces for tabs にチェックを入れる。tab コードの代わりにスペースを使う。(tab コードがお好きな方はそのままで)
  2. General -> Startup and Shutdown  で、RSE UI のチェックを外す。これは、意味不明の「RemoteSystemsTempFiles」を eclipse が勝手に作成するのをやめさせるため。
  3. General -> Workspace の Text file encoding を UTF-8 にする。(他の encoding が必要な場合は、それを設定してください)
  4. Remote Systems の Re-open Remote Sysmtes ... のチェックを外す。これも、意味不明の「RemoteSystemsTempFiles」を eclipse が勝手に作成するのをやめさせるため。
  5. PyDev -> Interpreters -> Python Interpreter で、自分の環境の Python にする。(env_jinja2/bin/python です)
  6. 続いて表示される PYTHONPATH の設定は、めんどうなので、すべてチェックを入れておく。(きちんと管理したい方はそのように)
  7. PyDev -> Interpreters -> Python Interpreter の String Substitution Variablesに名前が「run_pyramid」で、値が自分の環境にあるbin/pserveを追加する。

次に、先ほど作成した MyProject を eclipse のプロジェクトにしたいので、その名前で新たに Project を作成します。
Project name は MyProject にします。
つくるときに、Interpreter を先ほど設定した Python Interpreter にします。
eclipse の PyDev Package Explorer に MyProject が表示されるので、その中から development.ini を見つけて編集状態にします。
このままでは、MySQL とつながりにいってしまうので、その前に sqlite を使うようにします。
<修正前>
#SQlalchemy configuration (The prefix `sqlalchemy.` can be changed IF you change it in __init__.py's engine_from_config() call too
sqlalchemy.url=mysql+mysqldb://root:root123@localhost/test
sqlalchemy.pool_recycle = 3600
sqlalchemy.pool_size = 20
<修正後>
#SQlalchemy configuration (The prefix `sqlalchemy.` can be changed IF you change it in __init__.py's engine_from_config() call too
# sqlalchemy.url=mysql+mysqldb://root:root123@localhost/test
# sqlalchemy.pool_recycle = 3600
# sqlalchemy.pool_size = 20
sqlalchemy.url = sqlite:///%(here)s/MyProject.sqlite

次に MyProject を初期化して、pserve で起動してみる。
$ cd MyProject/
$ ../bin/python setup.py develop
$ ../bin/pserve development.ini 

「ImportError: No module named MySQLdb」って、言われたときは、先ほどの development.ini の修正を確認してください。

このままではなんだかわからないので、テンプレートを使って、ページ遷移もしてみます。
__init__.py を見ると「config.add_view」を使っているので、この辺りも修正します。

2014年4月18日金曜日

Pyramid 1.5 mako -> jinja2

Python Pyramid 1.4 を 1.5 にしたときのメモ。
テンプレートの扱いが変わってしまったので、ソースを修正しないと動かなくなってしまった。
テンプレートは mako を使用中。
環境についてはこう。
Mac OS X 10.9.2
Python 2.7.6

Pyramid については、ここを参照。

  1. development.ini
    テンプレートファイルが入っているディレクトリの設定はそのまま。
    mako.directories = my_project:templates

  2. setup.py
    ここは、requires に 'pyramid_mako' を追加する。
    requires = [
        'pyramid_mako',
        'pyramid',
    

  3. my_project/__init__.py
    config = Configurator(settings=settings, の後にこれを追加。
    config.include('pyramid_mako')
    

    テンプレートファイルの拡張子に .html を追加するところは、こう修正した。
    これがなかなかわからなかった。
    <修正前>
    config.add_renderer(".html", "pyramid.mako_templating.renderer_factory")
    
    <修正後>
    config.add_mako_renderer('.html', settings_prefix='mako.')
    

  4. views.py の renderer の設定は前のまま
    そのまま「renderer='home.html'」などとすることができる。
    @view_config(route_name='home', renderer='home.html', permission='edit', request_method="GET")
    

これで動くはず。
mako もいいけど jinja2 にしたい、という場合は「config.add_renderer()」のところと「mako.directories = pypwm:templates」をどうしたらよいのでしょう。
ついでにちょっとやってみる。

  1. development.ini
    pyramid.includes に pyramid_jinja2 を追加する。
    それと、jinja2.directories の設定。
    pyramid.includes =
        pyramid_jinja2
    #    pyramid_debugtoolbar
        pyramid_tm
    
    jinja2.directories = my_project:templates
    

  2. setup.py
    requires = [
        'pyramid_jinja2',
        'pyramid',
    

  3. my_project/__init__.py
    config.add_jinja2_search_path も追加する。
    config.include('pyramid_jinja2')
    config.add_renderer(".html", "pyramid_jinja2.renderer_factory")
    config.add_jinja2_search_path("pypwm:templates")
    


こんな感じでした。
mako テンプレートを jinja2 に書き換える方がたいへんです。

mako jinja2
コメント
## for maco
<%doc>multi line</%doc>
{# for jinja2 #}
{# multi line #}
include
<%include file="my_header.html"/>
{% include "my_header.html" %}
request.
static_url
など
"${request.static_url(
'pypwm:static/my.css')}"

${request.route_url('home')}
{{ request.application_url }}
/static/my.css"

{{ request.application_url }}/home
for loop
% for item in items:
% endfor
{% for item in items -%}
{%- endfor %}
if
% if xxx == 'yyy':
% else:
% endif
{% if xxx' == 'yyy' %}
{% else %}
{% endif %}
変数
${my_var}
{{ my_var }}

2014年1月28日火曜日

6. Python Pyramid + MySQL ついでに Mako から Jinja2

Djangoじゃないのをやってみようということで、Pyramidをさわってみる。
そのときの記録。
久しぶり。
今回は、Pyramid Tutorial を MySQL 化してみます。
Pyramid Tutorial の中の pyramid_blogr Tutorial をやってみます。このチュートリアルはテーブルをひとつではなく、複数使うのでこれを選びました。複数といってもふたつですが。

  • 環境
  • MacOS 10.9.1
    Python 2.7.6 (MacPortsでインストールしたものを使用する)
    MySQL 5.5.33 (MacPortsでインストールしたものを使用する)
    Apache 2.2.24 (標準でインストールされているもの)
    (mod_wsgi 3.4 はソースを落としてきてインストール)
    Eclipse 4.3.1
    Pydev 3.2.0

最初に、pyramid_blogr のソースをそのまま実行してみます。
pyramid_blogr をこつこつとつくっていくのは日を改めて。

  1. virtualenv を作成して、pyramid の環境を作る
    env_pyramid_blogr という名前で環境を作りました。
    $ virtualenv --no-site-packages env_pyramid_blogr

    つくったディレクトリに、pyramid をインストールします。
    $ cd env_pyramid_blogr/
    $ bin/pip install pyramid

  2. ソースのダウンロード
    git で clone します。
    $ git clone https://github.com/Pylons/pyramid_blogr

  3. sqlite で、まず実行
    $ cd pyramid_blogr/
    $ ../bin/python setup.py develop
    $ ../bin/pip install -e .
    $ ../bin/initialize_pyramid_blogr_db development.ini
    $ ../bin/pserve --reload development.ini
    

    localhost:6543 を見に行くと pyramid_blogr が動作しています。
    Users を admin、Password も admin で、Sign in できます。

    最初は空っぽなので、entry をつくってみます。

    Create a new blog entry をクリックすると、入力できるようになります。

    最初の画面に、entry title が表示されます。


  4. 日本語入れるとこける
    タイトルに日本語を入力するとこけます。

    この状態で保存すると「pyramid_mako.MakoRenderingException」と言われます。

    なんだかよくわかりませんが、ここんとこを修正すると直りました。
    models.py の slug() を修正します。「encode('utf-8')」を追加しました。
    -- OLD --
        @property
        def slug(self):
            return urlify(self.title)
    

    -- NEW --
        @property
        def slug(self):
            return urlify(self.title.encode('utf-8'))
    





で、これを MySQL にするには、ここを修正します。
  1. development.ini と production.ini のデータベースの接続設定を修正
    sqlite を mysql にする。

    -- OLD --
    sqlalchemy.url = sqlite:///%(here)s/pyramid_blogr.sqlite

    -- NEW --
    sqlalchemy.url = mysql://pyramid_user:password@localhost:3306/pyramid_blogr?charset=utf8&use_unicode=1
    sqlalchemy.pool_recycle = 3600

  2. pyramid_blogr/models.py にある型の import をしているところを修正
    Text, Unicode, UnicodeText をやめて、String にする。
    -- OLD --
    from sqlalchemy import (
         Column,
         Integer,
         Text,
         Unicode,     #<- will provide unicode field,
         UnicodeText, #<- will provide unicode text field,
         DateTime     #<- time abstraction field,
         )
    

    -- NEW --
    from sqlalchemy import (
         Column,
         Integer,
         String,
         DateTime     #<- time abstraction field,
         )
    

    そして、class User と class Entry にある項目の「Unicode(255)」や「UnicodeText」を「String(255)」に変更する。unique も取り除く。
    例えば、こう。
    -- OLD --
    title = Column(Unicode(255), unique=True, nullable=False)
    body = Column(UnicodeText, default=u'')

    -- NEW --
    title = Column(String(255), nullable=False)
    body = Column(String(255), default=u'')

  3. setup.py にある requires = [ に mysql-python を追加
    requires = [
        'pyramid',
        'pyramid_chameleon',
        'pyramid_debugtoolbar',
        'pyramid_tm',
        'SQLAlchemy',
        'transaction',
        'zope.sqlalchemy',
        'waitress',
        'wtforms',
        'webhelpers',
        'mysql-python',
        ]
    

これで修正は終了です。
動かしてみる前に、MySQL にデータベース「pyramid_blogr」を作成し、そのデータベースのためのユーザーを追加します。

MySQLに入る。
$ mysql -u root -p mysql

データベースの作成。
mysql> CREATE DATABASE pyramid_blogr CHARACTER SET utf8;

そのデータベースのユーザーの作成。
mysql> GRANT ALL ON pyramid_blogr.* TO pyramid_user@localhost IDENTIFIED by "password";


念のために、sqliete のデータファイル「pyramid_blogr.sqlite」を削除しておきます。
MySQL を使っていると言いつつも、このファイルができているときは sqlite をまだ使っているかも。
「development.ini」のあるディレクトリに「pyramid_blogr.sqlite」があります。
これからの作業をするために、そのディレクトリに移っておきます。

setup.py からもう一度。
$ ../bin/python setup.py develop
$ ../bin/pip install -e .
$ ../bin/initialize_pyramid_blogr_db development.ini
$ ../bin/pserve --reload development.ini

localhost:6543 を表示させると、sqlite の時と同じ Pyramid の画面が表示されます。



最後に、wsgi で動かせるようにします。localhost:6543 ではなく localhost/blogr で表示できます。
プロジェクト pyramid_blogr が置かれているディレクトリ、env_pyramid_blogr/ に pyramid.wsgi をつくります。
その内容はこう。このなかにある /Users/foo/env_pyramid_blogr の foo はあなたの名前(?)にしてください。
from pyramid.paster import get_app, setup_logging
ini_path = '/Users/foo/env_pyramid_blogr/pyramid_blogr/production.ini'
setup_logging(ini_path)
application = get_app(ini_path, 'main')

そして、mac os x 10.9 なら /etc/apache2/other/ にあるmodwsgi.conf を修正します。
env_first_app は、すでに動いているウェブアプリです。そんなものが無い人は、ここのところをばっさり切り取ってください。最初の「WSGIApplicationGroup %{GLOBAL}」の2行はそのままです。
それに、env_pyramid_blogr を追加します。
WSGIApplicationGroup %{GLOBAL}
WSGIPassAuthorization On

# env_first_app
WSGIDaemonProcess pyramid_env_app user=foo group=staff threads=4 \
   python-path=/Users/foo/env_app/lib/python2.7/site-packages
WSGIScriptAlias /app /Users/foo/env_app/pyramid.wsgi
<Directory /Users/foo/env_app>
  WSGIProcessGroup pyramid_env_app
  Order allow,deny
  Allow from all
</Directory>

# env_blogr
WSGIDaemonProcess pyramid_env_blogr user=foo group=staff threads=4 \
   python-path=/Users/foo/env_pyramid_blogr/lib/python2.7/site-packages
WSGIScriptAlias /blogr /Users/foo/env_pyramid_blogr/pyramid.wsgi
<Directory /Users/foo/env_pyramid_blogr >
  WSGIProcessGroup pyramid_env_blogr
  Order allow,deny
  Allow from all
</Directory>

これで、localhost/blogr を見に行くと・・・
こけます。
mako の rendering エラーです。
What's New In Pyramid 1.5 をみると、mako の扱いが変わってしまったようです。
なぜターミナルからなら動くのか不明ですが・・・。パスが通っているからか?
修正します。
setup.py はこう。「'pyramid_mako',」を requires に追加します。「pyramid_chameleon」はあるので、これを使え、ということか。
requires = [
    'pyramid',
    'pyramid_mako', # <--- !!!
    'pyramid_chameleon',

pyramid_blogr/__init__.py の main() に「config.include('pyramid_mako')」を追加します。
    config = Configurator(settings=settings,
                      authentication_policy=authentication_policy,
                      authorization_policy=authorization_policy
                      )
    config.include('pyramid_mako') # <--- !!!
    config.add_static_view('static', 'static', cache_max_age=3600)

これで動くようになりました。