From 1228fcef3c4dd42e60e610b9c2c3e9e05f117618 Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jan@dittberner.info>
Date: Sun, 5 Oct 2008 21:02:10 +0000
Subject: [PATCH] adding sqlalchemy-migrate glue  * add a product and
 producttype table (addresses #1)  * add a person table and reference to
 customers table (fixes #8)  * use sqlalchemy-migrate's API to setup database
 and add    configuration for the sqlalchemy-migrate calls to development.ini 
   and the paste_deploy template (fixes #7)

git-svn-id: file:///var/www/wwwusers/usr01/svn/pyalchemybiz/trunk@7 389c73d4-bf09-4d3d-a15e-f94a37d0667a
---
 data/dbrepo/README                            |  4 +++
 data/dbrepo/__init__.py                       |  0
 data/dbrepo/manage.py                         |  4 +++
 data/dbrepo/migrate.cfg                       | 20 ++++++++++++
 .../dbrepo/versions/001_Add_initial_tables.py | 29 +++++++++++++++++
 .../versions/002_Add_customer_tables.py       | 27 ++++++++++++++++
 data/dbrepo/versions/__init__.py              |  0
 development.ini                               |  4 +++
 pyalchemybiz.egg-info/PKG-INFO                |  2 +-
 pyalchemybiz.egg-info/SOURCES.txt             | 17 +++++++---
 .../paste_deploy_config.ini_tmpl              |  5 +++
 pyalchemybiz/config/environment.py            | 18 ++++++++++-
 pyalchemybiz/model/__init__.py                | 21 +++++++++++--
 pyalchemybiz/model/person.py                  |  5 +++
 pyalchemybiz/model/product.py                 |  8 +++++
 pyalchemybiz/websetup.py                      | 31 +++++++++++++++++--
 16 files changed, 183 insertions(+), 12 deletions(-)
 create mode 100644 data/dbrepo/README
 create mode 100644 data/dbrepo/__init__.py
 create mode 100644 data/dbrepo/manage.py
 create mode 100644 data/dbrepo/migrate.cfg
 create mode 100644 data/dbrepo/versions/001_Add_initial_tables.py
 create mode 100644 data/dbrepo/versions/002_Add_customer_tables.py
 create mode 100644 data/dbrepo/versions/__init__.py
 create mode 100644 pyalchemybiz/model/person.py
 create mode 100644 pyalchemybiz/model/product.py

diff --git a/data/dbrepo/README b/data/dbrepo/README
new file mode 100644
index 0000000..6218f8c
--- /dev/null
+++ b/data/dbrepo/README
@@ -0,0 +1,4 @@
+This is a database migration repository.
+
+More information at
+http://code.google.com/p/sqlalchemy-migrate/
diff --git a/data/dbrepo/__init__.py b/data/dbrepo/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/data/dbrepo/manage.py b/data/dbrepo/manage.py
new file mode 100644
index 0000000..b6ee059
--- /dev/null
+++ b/data/dbrepo/manage.py
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+from migrate.versioning.shell import main
+
+main(repository='data/dbrepo')
diff --git a/data/dbrepo/migrate.cfg b/data/dbrepo/migrate.cfg
new file mode 100644
index 0000000..f937e3f
--- /dev/null
+++ b/data/dbrepo/migrate.cfg
@@ -0,0 +1,20 @@
+[db_settings]
+# Used to identify which repository this database is versioned under.
+# You can use the name of your project.
+repository_id=pyalchemybiz
+
+# The name of the database table used to track the schema version.
+# This name shouldn't already be used by your project.
+# If this is changed once a database is under version control, you'll need to 
+# change the table name in each database too. 
+version_table=migrate_version
+
+# When committing a change script, Migrate will attempt to generate the 
+# sql for all supported databases; normally, if one of them fails - probably
+# because you don't have that database installed - it is ignored and the 
+# commit continues, perhaps ending successfully. 
+# Databases in this list MUST compile successfully during a commit, or the 
+# entire commit will fail. List the databases your application will actually 
+# be using to ensure your updates to that database work properly.
+# This must be a list; example: ['postgres','sqlite']
+required_dbs=[]
diff --git a/data/dbrepo/versions/001_Add_initial_tables.py b/data/dbrepo/versions/001_Add_initial_tables.py
new file mode 100644
index 0000000..8fa6843
--- /dev/null
+++ b/data/dbrepo/versions/001_Add_initial_tables.py
@@ -0,0 +1,29 @@
+from sqlalchemy import MetaData, Table, Column, ForeignKey, types
+from migrate import *
+
+def upgrade():
+    # Upgrade operations go here. Don't create your own engine; use the engine
+    # named 'migrate_engine' imported from migrate.
+    meta = MetaData(bind=migrate_engine)
+    t_product_type = Table(
+        'producttype', meta,
+        Column('id', types.Integer, primary_key=True),
+        Column('name', types.Unicode(40), nullable=False),
+        Column('description', types.UnicodeText(), nullable=False))
+    t_product_type.create()
+    t_product = Table(
+        'product', meta,
+        Column('id', types.Integer, primary_key=True),
+        Column('name', types.Unicode(100), nullable=False),
+        Column('description', types.UnicodeText(), nullable=False),
+        Column('producttype_id', types.Integer,
+               ForeignKey(t_product_type.c.id), nullable=False))
+    t_product.create()
+
+def downgrade():
+    # Operations to reverse the above upgrade go here.
+    meta = MetaData(bind=migrate_engine)
+    t_product = Table('product', meta, autoload=True)
+    t_product.drop()
+    t_product_type = Table('product_type', meta, autoload=True)
+    t_product_type.drop()
diff --git a/data/dbrepo/versions/002_Add_customer_tables.py b/data/dbrepo/versions/002_Add_customer_tables.py
new file mode 100644
index 0000000..192ac47
--- /dev/null
+++ b/data/dbrepo/versions/002_Add_customer_tables.py
@@ -0,0 +1,27 @@
+from sqlalchemy import MetaData, Table, Column, ForeignKey, types
+from migrate import *
+
+def upgrade():
+    # Upgrade operations go here. Don't create your own engine; use the engine
+    # named 'migrate_engine' imported from migrate.
+    meta = MetaData(bind=migrate_engine)
+    t_person = Table(
+        'person', meta,
+        Column('id', types.Integer, primary_key=True),
+        Column('firstname', types.Unicode(100), nullable=False),
+        Column('lastname', types.Unicode(100), nullable=False))
+    t_person.create()
+    t_customer = Table(
+        'customer', meta,
+        Column('id', types.Integer, primary_key=True),
+        Column('person_id', types.Integer, ForeignKey(t_person.c.id),
+               nullable=False, unique=True))
+    t_customer.create()
+
+def downgrade():
+    # Operations to reverse the above upgrade go here.
+    meta = MetaData(bind=migrate_engine)
+    t_customer = Table('customer', meta, autoload=True)
+    t_customer.drop()
+    t_person = Table('person', meta, autoload=True)
+    t_person.drop()
diff --git a/data/dbrepo/versions/__init__.py b/data/dbrepo/versions/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/development.ini b/development.ini
index acf16ee..08bcb9b 100644
--- a/development.ini
+++ b/development.ini
@@ -40,6 +40,10 @@ sqlalchemy.default.url = sqlite:///%(here)s/pyalchemybiz.db
 sqlalchemy.default.echo = true
 sqlalchemy.convert_unicode = true
 
+# settings for sqlalchemy-migrate
+migrate.repo.version = 2
+migrate.repo.dir = %(here)s/data/dbrepo
+
 
 # Logging configuration
 [loggers]
diff --git a/pyalchemybiz.egg-info/PKG-INFO b/pyalchemybiz.egg-info/PKG-INFO
index 957496c..6b3b986 100644
--- a/pyalchemybiz.egg-info/PKG-INFO
+++ b/pyalchemybiz.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.0
 Name: pyalchemybiz
-Version: 0.1dev-r5
+Version: 0.1dev-r6
 Summary: python based small business suite.
 Home-page: http://www.dittberner.info/projects/pyalchemybiz
 Author: Jan Dittberner
diff --git a/pyalchemybiz.egg-info/SOURCES.txt b/pyalchemybiz.egg-info/SOURCES.txt
index fd8780f..52dbaaa 100644
--- a/pyalchemybiz.egg-info/SOURCES.txt
+++ b/pyalchemybiz.egg-info/SOURCES.txt
@@ -4,8 +4,13 @@ development.ini
 setup.cfg
 setup.py
 test.ini
-data/templates/base.mako.py
-data/templates/customer.mako.py
+data/dbrepo/README
+data/dbrepo/__init__.py
+data/dbrepo/manage.py
+data/dbrepo/migrate.cfg
+data/dbrepo/versions/001_Add_initial_tables.py
+data/dbrepo/versions/002_Add_customer_tables.py
+data/dbrepo/versions/__init__.py
 docs/index.txt
 pyalchemybiz/__init__.py
 pyalchemybiz/websetup.py
@@ -24,6 +29,7 @@ pyalchemybiz/config/routing.py
 pyalchemybiz/controllers/__init__.py
 pyalchemybiz/controllers/customer.py
 pyalchemybiz/controllers/error.py
+pyalchemybiz/controllers/index.py
 pyalchemybiz/controllers/template.py
 pyalchemybiz/lib/__init__.py
 pyalchemybiz/lib/app_globals.py
@@ -32,10 +38,13 @@ pyalchemybiz/lib/helpers.py
 pyalchemybiz/model/__init__.py
 pyalchemybiz/model/customer.py
 pyalchemybiz/model/meta.py
-pyalchemybiz/public/index.html
+pyalchemybiz/model/person.py
+pyalchemybiz/model/product.py
 pyalchemybiz/templates/base.mako
 pyalchemybiz/templates/customer.mako
+pyalchemybiz/templates/index.mako
 pyalchemybiz/tests/__init__.py
 pyalchemybiz/tests/test_models.py
 pyalchemybiz/tests/functional/__init__.py
-pyalchemybiz/tests/functional/test_customer.py
\ No newline at end of file
+pyalchemybiz/tests/functional/test_customer.py
+pyalchemybiz/tests/functional/test_index.py
\ No newline at end of file
diff --git a/pyalchemybiz.egg-info/paste_deploy_config.ini_tmpl b/pyalchemybiz.egg-info/paste_deploy_config.ini_tmpl
index e5458c4..9772b1f 100644
--- a/pyalchemybiz.egg-info/paste_deploy_config.ini_tmpl
+++ b/pyalchemybiz.egg-info/paste_deploy_config.ini_tmpl
@@ -38,6 +38,11 @@ set debug = false
 # invalidate the URI when specifying a SQLite db via path name
 #sqlalchemy.default.url = sqlite:///%(here)s/pyalchemybiz.db
 #sqlalchemy.default.echo = false
+sqlalchemy.default.convert_unicode = true
+
+# settings for sqlalchemy-migrate
+migrate.repo.version = 2
+migrate.repo.dir = %(here)s/data/dbrepo
 
 
 # Logging configuration
diff --git a/pyalchemybiz/config/environment.py b/pyalchemybiz/config/environment.py
index 9658bd6..1c57d9d 100644
--- a/pyalchemybiz/config/environment.py
+++ b/pyalchemybiz/config/environment.py
@@ -3,6 +3,9 @@ import os
 
 from pylons import config
 from sqlalchemy import engine_from_config
+from sqlalchemy.exceptions import NoSuchTableError
+from migrate.versioning.api import db_version
+
 from pyalchemybiz.model import init_model
 
 import pyalchemybiz.lib.app_globals as app_globals
@@ -35,4 +38,17 @@ def load_environment(global_conf, app_conf):
     # any Pylons config options)
     engine = \
         engine_from_config(config, 'sqlalchemy.default.')
-    init_model(engine)
+
+    try:
+        init_model(engine)
+    except Exception:
+        # special handling for calls in websetup.py
+        import inspect
+        frame = inspect.currentframe()
+        try:
+            functions = [current[3] for current in \
+                             inspect.getouterframes(frame)]
+            if functions[1] != 'setup_config':
+                raise
+        finally:
+            del frame
diff --git a/pyalchemybiz/model/__init__.py b/pyalchemybiz/model/__init__.py
index b4c8fe7..7309801 100644
--- a/pyalchemybiz/model/__init__.py
+++ b/pyalchemybiz/model/__init__.py
@@ -1,8 +1,11 @@
+import logging
 import sqlalchemy as sa
 from sqlalchemy import orm
 
 from pyalchemybiz.model import meta
-from pyalchemybiz.model import customer
+from pyalchemybiz.model import person, customer, product
+
+log = logging.getLogger(__name__)
 
 def init_model(engine):
     """Call me before using any of the tables or classes in the model."""
@@ -11,7 +14,19 @@ def init_model(engine):
 
     meta.engine = engine
     meta.Session = orm.scoped_session(sm)
+    person.t_person = sa.Table(
+        'person', meta.metadata, autoload=True, autoload_with=engine)
+    customer.t_customer = sa.Table(
+        'customer', meta.metadata, autoload=True, autoload_with=engine)
+    product.t_producttype = sa.Table(
+        'producttype', meta.metadata, autoload=True, autoload_with=engine)
+    product.t_product = sa.Table(
+        'product', meta.metadata, autoload=True, autoload_with=engine)
 
-    customer.t_customer = sa.Table('customer', meta.metadata,
-                                   autoload=True, autoload_with=engine)
+    orm.mapper(person.Person, person.t_person)
     orm.mapper(customer.Customer, customer.t_customer)
+    customer.Customer.person = orm.relation(person.Person)
+    orm.mapper(product.ProductType, product.t_producttype)
+    orm.mapper(product.Product, product.t_product)
+    product.Product.producttype = orm.relation(
+        product.Product, backref=orm.backref('products', lazy='dynamic'))
diff --git a/pyalchemybiz/model/person.py b/pyalchemybiz/model/person.py
new file mode 100644
index 0000000..d9ee9b4
--- /dev/null
+++ b/pyalchemybiz/model/person.py
@@ -0,0 +1,5 @@
+t_person = None
+
+class Person(object):
+    def __str__(self):
+        return "%s %s" % (self.firstname, self.lastname)
diff --git a/pyalchemybiz/model/product.py b/pyalchemybiz/model/product.py
new file mode 100644
index 0000000..866bf70
--- /dev/null
+++ b/pyalchemybiz/model/product.py
@@ -0,0 +1,8 @@
+t_producttype = None
+t_product = None
+
+class ProductType(object):
+    pass
+
+class Product(object):
+    pass
diff --git a/pyalchemybiz/websetup.py b/pyalchemybiz/websetup.py
index 5504807..f312c82 100644
--- a/pyalchemybiz/websetup.py
+++ b/pyalchemybiz/websetup.py
@@ -4,6 +4,10 @@ import logging
 from paste.deploy import appconfig
 from pylons import config
 
+from sqlalchemy.exceptions import NoSuchTableError
+import sys
+from migrate.versioning.api import db_version, version_control, upgrade
+
 from pyalchemybiz.config.environment import load_environment
 
 log = logging.getLogger(__name__)
@@ -13,10 +17,31 @@ def setup_config(command, filename, section, vars):
     conf = appconfig('config:' + filename)
     load_environment(conf.global_conf, conf.local_conf)
 
+    repoversion = int(config.get('migrate.repo.version'))
+    repodir = config.get('migrate.repo.dir')
+    dburl = config.get('sqlalchemy.default.url')
+
     # Populate the DB on 'paster setup-app'
-    import pyalchemybiz.model as model
 
     log.info("Setting up database connectivity...")
-    log.info("Creating tables...")
-    model.meta.metadata.create_all(bind=model.meta.engine)
+    log.info("Desired database repository version: %d" % repoversion)
+    log.info("Desired database repository directory: %s" % repodir)
+
+    try:
+        dbversion = int(db_version(dburl, repodir))
+    except NoSuchTableError:
+        version_control(dburl, repodir)
+        dbversion = int(db_version(dburl, repodir))
+    except Exception, e:
+        log.error(e)
+        raise e
+    log.info("detected db version %d" % dbversion)
+    if dbversion < repoversion:
+        upgrade(dburl, repodir, repoversion)
+    elif dbversion > repoversion:
+        log.error("The database at %s is already versioned and its version " +
+                  "%d is greater than the required version %d",
+                  dburl, dbversion, repoversion)
+        sys.exit(1)
+
     log.info("Successfully set up.")