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.")