authored by Wayne Witzel III

Working with Pyramid and Ming

On January 10, 2012 In python, ming Permalink

As I've been working on Community Cookbook I have encountered some situations using Ming that I am sure many other people have as well. Using Ming as my ODM and I have been very happy with how it is going so I wanted to share with everyone the steps I have taken to integrate Ming into Pyramid. My goal was to make it feel just like you would except a data storage integration to feel. Simple, clean, and easy.

As part of creating this integration, I created my own scaffold to hook everything up. You can download and try the scaffold yourself from my SourceForge repository. Below I will outline some of the key areas I had to configure. I highly reccomend you install and create a sample project with the scaffold when you follow along with this blog post.

First in the main init.py of the project, I had to setup some configuration. If you have created a new project using the scaffold this will be done for you. Here are the important snippets from root init.py

# stockpot/__init__.py

from pyramid.events import NewRequest
import stockpot.models as M

def main(global_config, **settings):
    # ...
    config.begin()
    config.scan('stockpot.models')
    M.init_mongo(engine=(settings.get('mongo.url'), settings.get('mongo.database')))
    config.add_subscriber(close_mongo_db, NewRequest)
    # ...

def close_mongo_db(event):
    def close(request):
         M.DBSession.close_all()
    event.request.add_finished_callback(close)

Now lets take a look at what you are getting from the models import (M). We use init mongo which does exactly what you think and then we use DBSession which I used as a name since it is familar to those who are coming from SQLalchemy. I will just show you the entire file.

# stockpot/models/__init__.py

from ming import Session
from ming.datastore import DataStore
from ming.orm import ThreadLocalORMSession
from ming.orm import Mapper

session = Session
DBSession = ThreadLocalORMSession(session)

def init_mongo(engine):
    server, database = engine
    datastore = DataStore(server, database=database)
    session.bind = datastore
    Mapper.compile_all()

# Here we just ensure indexes on all our mappers at startup.
    for mapper in Mapper.all_mappers():
        session.ensure_indexes(mapper.collection)

# Flush changes and close all connections
    DBSession.flush()
    DBSession.close_all()

from .user import User

Now I want to show you how I did the groupfinder and RequestWithAttribute. Since I imagine most of you looking to use Ming will probably have some type of user model. For the groupfinder and RequestWithAttributes the only special thing I had to do was add an extra step to convert the users id in to a proper bson.ObjectId

from pyramid.decorator import reify
from pyramid.request import Request
from pyramid.security import unauthenticated_userid

import bson
import stockpot.models as M

def groupfinder(userid, request):
    userid = bson.ObjectId(userid)
    user = M.User.query.get(_id=userid)
    return [] if user else None

class RequestWithAttributes(Request):
    @reify
    def user(self):
        userid = unauthenticated_userid(self)
    userid = bson.ObjectId(userid)
    if userid:
        return M.User.query.get(_id=userid)
    return None

Finally here is what my mapped User model might look like. This is a sample taken from a real project with some sensitive elements replaced and/or removed. Use it as an example, but not as a good one.

# stockpot/models/user.py

from hashlib import sha1
from datetime import datetime
from string import ascii_letters, digits
from random import choice

from ming import schema as S
from ming.orm import FieldProperty
from ming.orm.declarative import MappedClass

from pyramid.httpexceptions import HTTPForbidden

from stockpot.models import DBSession

# If you change this AFTER a user signed up they will not be able to
# login until they perform a password reset.
SALT = 'supersecretsalt'
CHARS = ascii_letters + digits
MAX_TRIES = 100

class User(MappedClass):
    class __mongometa__:
        session = DBSession
        name = 'users'
        custom_indexes = [
                dict(fields=('email',), unique=True, sparse=False),
                dict(fields=('username',), unique=True, sparse=False),
                dict(fields=('identifier',), unique=True, sparse=False),
        ]

# This should feel familar to anyone coming from SQLa
    _id = FieldProperty(S.ObjectId)
    username = FieldProperty(str)
    email = FieldProperty(str)
    password = FieldProperty(str, if_missing=S.Missing)
    signup_date = FieldProperty(datetime, if_missing=datetime.utcnow())

# Simple init method
    def __init__(self, *args, **kwargs):
        self.signup_date = datetime.utcnow().replace(microsecond=0)
        self.username = kwargs.get('username')
        if kwargs.get('password'):
            self.password = User.generate_password(kwargs.get('password'),
                    str(self.signup_date))
        self.email = kwargs.get('email', '{0}@example.com'.format(self.username))

# Update method is a little different than you are used to
    # ensure by the time you're calling this everything is validated and safe
    def update(self, *args, **kwargs):
        for k,v in kwargs.items():
            if k == 'password':
                v = User.generate_password(v, str(self.signup_date))
            setattr(self, k, v)

# Standard authenticate method
    @classmethod
    def authenticate(cls, login, password):
        user = cls.query.find({'$or': [{'username':login}, {'email':login}]}).one()
        if user:
            password = User.generate_password(password, str(user.signup_date))
            if password == user.password:
                return user
        else:
            return None

# Password hashing
    @staticmethod
    def generate_password(password, salt):
        password = sha1(password).hexdigest() + salt
        return sha1(password+SALT).hexdigest()

# Username generation, the username generation is very useful if you use social auth
    @staticmethod
    def random_username(_range=5, prefix=''):
        return prefix + ''.join([choice(CHARS) for i in range(_range)])

And there you have it, if you tie all the elements together you get a pretty easy and straightforward Ming integration with Pyramid.

Read and Post Comments
blog comments powered by Disqus