migrate.versioning PEP-8 improvements, more documentation

- made api.py, cfgparse.py, exceptions.py, genmodel.py,
  migrate_repository.py, and pathed.py PEP-8 clean
- add tools.rst documenting the usage of migrate_repository.py
- add more modules to api.rst
- reference tools.rst from index.rst
This commit is contained in:
jan.dittberner 2009-01-25 21:08:33 +00:00
parent 5be16f226f
commit 5289c4df3b
10 changed files with 406 additions and 209 deletions

View File

@ -84,3 +84,30 @@ Module :mod:`migrate.versioning`
.. automodule:: migrate.versioning .. automodule:: migrate.versioning
:members: :members:
:synopsis: Database version and repository management :synopsis: Database version and repository management
Module :mod:`api <migrate.versioning.api>`
------------------------------------------
.. automodule:: migrate.versioning.api
:synopsis: External API for :mod:`migrate.versioning`
Module :mod:`exceptions <migrate.versioning.exceptions>`
--------------------------------------------------------
.. automodule:: migrate.versioning.exceptions
:members:
:synopsis: Exception classes for :mod:`migrate.versioning`
Module :mod:`genmodel <migrate.versioning.genmodel>`
----------------------------------------------------
.. automodule:: migrate.versioning.genmodel
:members:
:synopsis: Python database model generator and differencer
Module :mod:`pathed <migrate.versioning.pathed>`
------------------------------------------------
.. automodule:: migrate.versioning.pathed
:members:
:synopsis: File/Directory handling class

View File

@ -28,6 +28,7 @@ versioning API is available as the :command:`migrate` command.
versioning versioning
changeset changeset
tools
api api
.. _`google's summer of code`: http://code.google.com/soc .. _`google's summer of code`: http://code.google.com/soc

19
docs/tools.rst Normal file
View File

@ -0,0 +1,19 @@
SQLAlchemy migrate tools
========================
The most commonly used tool is the :command:`migrate` script that is
discussed in depth in the :ref:`Database schema versioning
<versioning-system>` part of the documentation.
.. index:: repository migration
There is a second tool :command:`migrate_repository.py` that may be
used to migrate your repository from a version before 0.4.5 of
SQLAlchemy migrate to the current version.
.. module:: migrate.versioning.migrate_repository
:synopsis: Tool for migrating pre 0.4.5 repositories to current layout
Running :command:`migrate_repository.py` is as easy as:
:samp:`migrate_repository.py {repository_directory}`

View File

@ -1,7 +1,5 @@
""" """
Module migrate.versioning This package provides functionality to create and manage
------------------------- repositories of database schema changesets and to apply these
changesets to databases.
This package provides functionality to create and manage repositories of
database schema changesets and to apply these changesets to databases.
""" """

View File

@ -1,10 +1,21 @@
"""An external API to the versioning system
Used by the shell utility; could also be used by other scripts
""" """
This module provides an external API to the versioning system.
Used by the shell utility; could also be used by other scripts
"""
# Dear migrate developers,
#
# please do not comment this module using sphinx syntax because its
# docstrings are presented as user help and most users cannot
# interpret sphinx annotated ReStructuredText.
#
# Thanks,
# Jan Dittberner
import sys import sys
import inspect import inspect
from sqlalchemy import create_engine from sqlalchemy import create_engine
from migrate.versioning import exceptions,repository,schema,version from migrate.versioning import exceptions, repository, schema, version
import script as script_ #command name conflict import script as script_ #command name conflict
__all__=[ __all__=[
@ -32,7 +43,8 @@ cls_schema = schema.ControlledSchema
cls_vernum = version.VerNum cls_vernum = version.VerNum
cls_script_python = script_.PythonScript cls_script_python = script_.PythonScript
def help(cmd=None,**opts):
def help(cmd=None, **opts):
"""%prog help COMMAND """%prog help COMMAND
Displays help on a given command. Displays help on a given command.
@ -42,57 +54,74 @@ def help(cmd=None,**opts):
try: try:
func = globals()[cmd] func = globals()[cmd]
except: except:
raise exceptions.UsageError("'%s' isn't a valid command. Try 'help COMMAND'"%cmd) raise exceptions.UsageError(
"'%s' isn't a valid command. Try 'help COMMAND'" % cmd)
ret = func.__doc__ ret = func.__doc__
if sys.argv[0]: if sys.argv[0]:
ret = ret.replace('%prog',sys.argv[0]) ret = ret.replace('%prog', sys.argv[0])
return ret return ret
def create(repository,name,**opts):
def create(repository, name, **opts):
"""%prog create REPOSITORY_PATH NAME [--table=TABLE] """%prog create REPOSITORY_PATH NAME [--table=TABLE]
Create an empty repository at the specified path. Create an empty repository at the specified path.
You can specify the version_table to be used; by default, it is '_version'. You can specify the version_table to be used; by default, it is
This table is created in all version-controlled databases. '_version'. This table is created in all version-controlled
databases.
""" """
try: try:
rep=cls_repository.create(repository,name,**opts) rep=cls_repository.create(repository, name, **opts)
except exceptions.PathFoundError,e: except exceptions.PathFoundError, e:
raise exceptions.KnownError("The path %s already exists"%e.args[0]) raise exceptions.KnownError("The path %s already exists" % e.args[0])
def script(description,repository=None,**opts):
def script(description, repository=None, **opts):
"""%prog script [--repository=REPOSITORY_PATH] DESCRIPTION """%prog script [--repository=REPOSITORY_PATH] DESCRIPTION
Create an empty change script using the next unused version number appended with the given description. Create an empty change script using the next unused version number
For instance, manage.py script "Add initial tables" creates: repository/versions/001_Add_initial_tables.py appended with the given description.
For instance, manage.py script "Add initial tables" creates:
repository/versions/001_Add_initial_tables.py
""" """
try: try:
if repository is None: if repository is None:
raise exceptions.UsageError("A repository must be specified") raise exceptions.UsageError("A repository must be specified")
repos = cls_repository(repository) repos = cls_repository(repository)
repos.create_script(description,**opts) repos.create_script(description, **opts)
except exceptions.PathFoundError,e: except exceptions.PathFoundError, e:
raise exceptions.KnownError("The path %s already exists"%e.args[0]) raise exceptions.KnownError("The path %s already exists"%e.args[0])
def script_sql(database,repository=None,**opts):
def script_sql(database, repository=None, **opts):
"""%prog script_sql [--repository=REPOSITORY_PATH] DATABASE """%prog script_sql [--repository=REPOSITORY_PATH] DATABASE
Create empty change SQL scripts for given DATABASE, where DATABASE is either specific ('postgres', 'mysql', Create empty change SQL scripts for given DATABASE, where DATABASE
'oracle', 'sqlite', etc.) or generic ('default'). is either specific ('postgres', 'mysql', 'oracle', 'sqlite', etc.)
or generic ('default').
For instance, manage.py script_sql postgres creates: For instance, manage.py script_sql postgres creates:
repository/versions/001_upgrade_postgres.py and repository/versions/001_downgrade_postgres.py repository/versions/001_upgrade_postgres.py and
repository/versions/001_downgrade_postgres.py
""" """
try: try:
if repository is None: if repository is None:
raise exceptions.UsageError("A repository must be specified") raise exceptions.UsageError("A repository must be specified")
repos = cls_repository(repository) repos = cls_repository(repository)
repos.create_script_sql(database,**opts) repos.create_script_sql(database, **opts)
except exceptions.PathFoundError,e: except exceptions.PathFoundError, e:
raise exceptions.KnownError("The path %s already exists"%e.args[0]) raise exceptions.KnownError("The path %s already exists"%e.args[0])
def test(repository,url=None,**opts):
def test(repository, url=None, **opts):
"""%prog test REPOSITORY_PATH URL [VERSION] """%prog test REPOSITORY_PATH URL [VERSION]
Performs the upgrade and downgrade option on the given
database. This is not a real test and may leave the database in a
bad state. You should therefore better run the test on a copy of
your database.
""" """
engine=create_engine(url) engine=create_engine(url)
repos=cls_repository(repository) repos=cls_repository(repository)
@ -100,7 +129,7 @@ def test(repository,url=None,**opts):
# Upgrade # Upgrade
print "Upgrading...", print "Upgrading...",
try: try:
script.run(engine,1) script.run(engine, 1)
except: except:
print "ERROR" print "ERROR"
raise raise
@ -108,14 +137,15 @@ def test(repository,url=None,**opts):
print "Downgrading...", print "Downgrading...",
try: try:
script.run(engine,-1) script.run(engine, -1)
except: except:
print "ERROR" print "ERROR"
raise raise
print "done" print "done"
print "Success" print "Success"
def version(repository,**opts):
def version(repository, **opts):
"""%prog version REPOSITORY_PATH """%prog version REPOSITORY_PATH
Display the latest version available in a repository. Display the latest version available in a repository.
@ -123,111 +153,124 @@ def version(repository,**opts):
repos=cls_repository(repository) repos=cls_repository(repository)
return repos.latest return repos.latest
def source(version,dest=None,repository=None,**opts):
def source(version, dest=None, repository=None, **opts):
"""%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH
Display the Python code for a particular version in this repository. Display the Python code for a particular version in this
Save it to the file at DESTINATION or, if omitted, send to stdout. repository. Save it to the file at DESTINATION or, if omitted,
send to stdout.
""" """
if repository is None: if repository is None:
raise exceptions.UsageError("A repository must be specified") raise exceptions.UsageError("A repository must be specified")
repos=cls_repository(repository) repos=cls_repository(repository)
ret=repos.version(version).script().source() ret=repos.version(version).script().source()
if dest is not None: if dest is not None:
dest=open(dest,'w') dest=open(dest, 'w')
dest.write(ret) dest.write(ret)
ret=None ret=None
return ret return ret
def version_control(url,repository,version=None,**opts):
def version_control(url, repository, version=None, **opts):
"""%prog version_control URL REPOSITORY_PATH [VERSION] """%prog version_control URL REPOSITORY_PATH [VERSION]
Mark a database as under this repository's version control. Mark a database as under this repository's version control.
Once a database is under version control, schema changes should only be
done via change scripts in this repository. Once a database is under version control, schema changes should
only be done via change scripts in this repository.
This creates the table version_table in the database. This creates the table version_table in the database.
The url should be any valid SQLAlchemy connection string. The url should be any valid SQLAlchemy connection string.
By default, the database begins at version 0 and is assumed to be empty. By default, the database begins at version 0 and is assumed to be
If the database is not empty, you may specify a version at which to begin empty. If the database is not empty, you may specify a version at
instead. No attempt is made to verify this version's correctness - the which to begin instead. No attempt is made to verify this
database schema is expected to be identical to what it would be if the version's correctness - the database schema is expected to be
database were created from scratch. identical to what it would be if the database were created from
scratch.
""" """
echo = 'True' == opts.get('echo', False) echo = 'True' == opts.get('echo', False)
engine = create_engine(url, echo=echo) engine = create_engine(url, echo=echo)
cls_schema.create(engine,repository,version) cls_schema.create(engine, repository, version)
def db_version(url,repository,**opts):
def db_version(url, repository, **opts):
"""%prog db_version URL REPOSITORY_PATH """%prog db_version URL REPOSITORY_PATH
Show the current version of the repository with the given connection Show the current version of the repository with the given
string, under version control of the specified repository. connection string, under version control of the specified
repository.
The url should be any valid SQLAlchemy connection string. The url should be any valid SQLAlchemy connection string.
""" """
echo = 'True' == opts.get('echo', False) echo = 'True' == opts.get('echo', False)
engine = create_engine(url, echo=echo) engine = create_engine(url, echo=echo)
schema = cls_schema(engine,repository) schema = cls_schema(engine, repository)
return schema.version return schema.version
def upgrade(url,repository,version=None,**opts):
def upgrade(url, repository, version=None, **opts):
"""%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql] """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql]
Upgrade a database to a later version. Upgrade a database to a later version.
This runs the upgrade() function defined in your change scripts. This runs the upgrade() function defined in your change scripts.
By default, the database is updated to the latest available version. You By default, the database is updated to the latest available
may specify a version instead, if you wish. version. You may specify a version instead, if you wish.
You may preview the Python or SQL code to be executed, rather than actually You may preview the Python or SQL code to be executed, rather than
executing it, using the appropriate 'preview' option. actually executing it, using the appropriate 'preview' option.
""" """
err = "Cannot upgrade a database of version %s to version %s. "\ err = "Cannot upgrade a database of version %s to version %s. "\
"Try 'downgrade' instead." "Try 'downgrade' instead."
return _migrate(url,repository,version,upgrade=True,err=err,**opts) return _migrate(url, repository, version, upgrade=True, err=err, **opts)
def downgrade(url,repository,version,**opts):
def downgrade(url, repository, version, **opts):
"""%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql] """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql]
Downgrade a database to an earlier version. Downgrade a database to an earlier version.
This is the reverse of upgrade; this runs the downgrade() function defined
in your change scripts.
You may preview the Python or SQL code to be executed, rather than actually This is the reverse of upgrade; this runs the downgrade() function
executing it, using the appropriate 'preview' option. defined in your change scripts.
You may preview the Python or SQL code to be executed, rather than
actually executing it, using the appropriate 'preview' option.
""" """
err = "Cannot downgrade a database of version %s to version %s. "\ err = "Cannot downgrade a database of version %s to version %s. "\
"Try 'upgrade' instead." "Try 'upgrade' instead."
return _migrate(url,repository,version,upgrade=False,err=err,**opts) return _migrate(url, repository, version, upgrade=False, err=err, **opts)
def _migrate(url,repository,version,upgrade,err,**opts):
def _migrate(url, repository, version, upgrade, err, **opts):
echo = 'True' == opts.get('echo', False) echo = 'True' == opts.get('echo', False)
engine = create_engine(url, echo=echo) engine = create_engine(url, echo=echo)
schema = cls_schema(engine,repository) schema = cls_schema(engine, repository)
version = _migrate_version(schema,version,upgrade,err) version = _migrate_version(schema, version, upgrade, err)
changeset = schema.changeset(version) changeset = schema.changeset(version)
for ver,change in changeset: for ver, change in changeset:
nextver = ver + changeset.step nextver = ver + changeset.step
print '%s -> %s... '%(ver,nextver), print '%s -> %s... '%(ver, nextver),
if opts.get('preview_sql'): if opts.get('preview_sql'):
print print
print change.log print change.log
elif opts.get('preview_py'): elif opts.get('preview_py'):
source_ver = max(ver,nextver) source_ver = max(ver, nextver)
module = schema.repository.version(source_ver).script().module module = schema.repository.version(source_ver).script().module
funcname = upgrade and "upgrade" or "downgrade" funcname = upgrade and "upgrade" or "downgrade"
func = getattr(module,funcname) func = getattr(module, funcname)
print print
print inspect.getsource(module.upgrade) print inspect.getsource(module.upgrade)
else: else:
schema.runchange(ver,change,changeset.step) schema.runchange(ver, change, changeset.step)
print 'done' print 'done'
def _migrate_version(schema,version,upgrade,err):
def _migrate_version(schema, version, upgrade, err):
if version is None: if version is None:
return version return version
# Version is specified: ensure we're upgrading in the right direction # Version is specified: ensure we're upgrading in the right direction
@ -240,48 +283,54 @@ def _migrate_version(schema,version,upgrade,err):
else: else:
direction = cur >= version direction = cur >= version
if not direction: if not direction:
raise exceptions.KnownError(err%(cur,version)) raise exceptions.KnownError(err%(cur, version))
return version return version
def drop_version_control(url,repository,**opts):
def drop_version_control(url, repository, **opts):
"""%prog drop_version_control URL REPOSITORY_PATH """%prog drop_version_control URL REPOSITORY_PATH
Removes version control from a database. Removes version control from a database.
""" """
echo = 'True' == opts.get('echo', False) echo = 'True' == opts.get('echo', False)
engine = create_engine(url, echo=echo) engine = create_engine(url, echo=echo)
schema=cls_schema(engine,repository) schema=cls_schema(engine, repository)
schema.drop() schema.drop()
def manage(file,**opts):
def manage(file, **opts):
"""%prog manage FILENAME VARIABLES... """%prog manage FILENAME VARIABLES...
Creates a script that runs Migrate with a set of default values. Creates a script that runs Migrate with a set of default values.
For example:: For example::
%prog manage manage.py --repository=/path/to/repository --url=sqlite:///project.db %prog manage manage.py --repository=/path/to/repository \
--url=sqlite:///project.db
would create the script manage.py. The following two commands would then would create the script manage.py. The following two commands
have exactly the same results:: would then have exactly the same results::
python manage.py version python manage.py version
%prog version --repository=/path/to/repository %prog version --repository=/path/to/repository
""" """
return repository.manage(file,**opts) return repository.manage(file, **opts)
def compare_model_to_db(url,model,repository,**opts):
def compare_model_to_db(url, model, repository, **opts):
"""%prog compare_model_to_db URL MODEL REPOSITORY_PATH """%prog compare_model_to_db URL MODEL REPOSITORY_PATH
Compare the current model (assumed to be a module level variable of type sqlalchemy.MetaData) against the current database. Compare the current model (assumed to be a module level variable
of type sqlalchemy.MetaData) against the current database.
NOTE: This is EXPERIMENTAL. NOTE: This is EXPERIMENTAL.
""" # TODO: get rid of EXPERIMENTAL label """ # TODO: get rid of EXPERIMENTAL label
echo = 'True' == opts.get('echo', False) echo = 'True' == opts.get('echo', False)
engine = create_engine(url, echo=echo) engine = create_engine(url, echo=echo)
print cls_schema.compare_model_to_db(engine,model,repository) print cls_schema.compare_model_to_db(engine, model, repository)
def create_model(url,repository,**opts):
def create_model(url, repository, **opts):
"""%prog create_model URL REPOSITORY_PATH """%prog create_model URL REPOSITORY_PATH
Dump the current database as a Python model to stdout. Dump the current database as a Python model to stdout.
@ -291,32 +340,37 @@ def create_model(url,repository,**opts):
echo = 'True' == opts.get('echo', False) echo = 'True' == opts.get('echo', False)
engine = create_engine(url, echo=echo) engine = create_engine(url, echo=echo)
declarative = opts.get('declarative', False) declarative = opts.get('declarative', False)
print cls_schema.create_model(engine,repository,declarative) print cls_schema.create_model(engine, repository, declarative)
def make_update_script_for_model(url,oldmodel,model,repository,**opts):
def make_update_script_for_model(url, oldmodel, model, repository, **opts):
"""%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH
Create a script changing the old Python model to the new (current) Python model, sending to stdout. Create a script changing the old Python model to the new (current)
Python model, sending to stdout.
NOTE: This is EXPERIMENTAL. NOTE: This is EXPERIMENTAL.
""" # TODO: get rid of EXPERIMENTAL label """ # TODO: get rid of EXPERIMENTAL label
echo = 'True' == opts.get('echo', False) echo = 'True' == opts.get('echo', False)
engine = create_engine(url, echo=echo) engine = create_engine(url, echo=echo)
try: try:
print cls_script_python.make_update_script_for_model(engine,oldmodel,model,repository,**opts) print cls_script_python.make_update_script_for_model(
except exceptions.PathFoundError,e: engine, oldmodel, model, repository, **opts)
raise exceptions.KnownError("The path %s already exists"%e.args[0]) # TODO: get rid of this? if we don't add back path param except exceptions.PathFoundError, e:
# TODO: get rid of this? if we don't add back path param
raise exceptions.KnownError("The path %s already exists" % e.args[0])
def update_db_from_model(url,model,repository,**opts):
def update_db_from_model(url, model, repository, **opts):
"""%prog update_db_from_model URL MODEL REPOSITORY_PATH """%prog update_db_from_model URL MODEL REPOSITORY_PATH
Modify the database to match the structure of the current Python model. Modify the database to match the structure of the current Python
This also sets the db_version number to the latest in the repository. model. This also sets the db_version number to the latest in the
repository.
NOTE: This is EXPERIMENTAL. NOTE: This is EXPERIMENTAL.
""" # TODO: get rid of EXPERIMENTAL label """ # TODO: get rid of EXPERIMENTAL label
echo = 'True' == opts.get('echo', False) echo = 'True' == opts.get('echo', False)
engine = create_engine(url, echo=echo) engine = create_engine(url, echo=echo)
schema = cls_schema(engine,repository) schema = cls_schema(engine, repository)
schema.update_db_from_model(model) schema.update_db_from_model(model)

View File

@ -1,19 +1,26 @@
"""
Configuration parser module.
"""
from migrate.versioning.base import * from migrate.versioning.base import *
from migrate.versioning import pathed from migrate.versioning import pathed
from ConfigParser import ConfigParser from ConfigParser import ConfigParser
#__all__=['MigrateConfigParser']
class Parser(ConfigParser): class Parser(ConfigParser):
"""A project configuration file""" """A project configuration file."""
def to_dict(self,sections=None):
def to_dict(self, sections=None):
"""It's easier to access config values like dictionaries""" """It's easier to access config values like dictionaries"""
return self._sections return self._sections
class Config(pathed.Pathed,Parser):
def __init__(self,path,*p,**k): class Config(pathed.Pathed, Parser):
"""Confirm the config file exists; read it""" """Configuration class."""
def __init__(self, path, *p, **k):
"""Confirm the config file exists; read it."""
self.require_found(path) self.require_found(path)
pathed.Pathed.__init__(self,path) pathed.Pathed.__init__(self, path)
Parser.__init__(self,*p,**k) Parser.__init__(self, *p, **k)
self.read(path) self.read(path)

View File

@ -1,32 +1,60 @@
"""
Provide exception classes for :mod:`migrate.versioning`
"""
class Error(Exception): class Error(Exception):
"""Error base class."""
pass pass
class ApiError(Error): class ApiError(Error):
"""Base class for API errors."""
pass pass
class KnownError(ApiError): class KnownError(ApiError):
"""A known error condition""" """A known error condition."""
class UsageError(ApiError): class UsageError(ApiError):
"""A known error condition where help should be displayed""" """A known error condition where help should be displayed."""
class ControlledSchemaError(Error): class ControlledSchemaError(Error):
pass """Base class for controlled schema errors."""
class InvalidVersionError(ControlledSchemaError):
"""Invalid version number"""
class DatabaseNotControlledError(ControlledSchemaError):
"""Database shouldn't be under vc, but it is"""
class DatabaseAlreadyControlledError(ControlledSchemaError):
"""Database should be under vc, but it's not"""
class WrongRepositoryError(ControlledSchemaError):
"""This database is under version control by another repository"""
class NoSuchTableError(ControlledSchemaError):
pass pass
class InvalidVersionError(ControlledSchemaError):
"""Invalid version number."""
class DatabaseNotControlledError(ControlledSchemaError):
"""Database should be under version control, but it's not."""
class DatabaseAlreadyControlledError(ControlledSchemaError):
"""Database shouldn't be under version control, but it is"""
class WrongRepositoryError(ControlledSchemaError):
"""This database is under version control by another repository."""
class NoSuchTableError(ControlledSchemaError):
"""The table does not exist."""
pass
class LogSqlError(Error): class LogSqlError(Error):
"""A SQLError, with a traceback of where that statement was logged""" """A SQLError, with a traceback of where that statement was logged."""
def __init__(self,sqlerror,entry):
def __init__(self, sqlerror, entry):
Exception.__init__(self) Exception.__init__(self)
self.sqlerror = sqlerror self.sqlerror = sqlerror
self.entry = entry self.entry = entry
def __str__(self): def __str__(self):
ret = "SQL error in statement: \n%s\n"%(str(self.entry)) ret = "SQL error in statement: \n%s\n"%(str(self.entry))
ret += "Traceback from change script:\n" ret += "Traceback from change script:\n"
@ -34,25 +62,42 @@ class LogSqlError(Error):
ret += str(self.sqlerror) ret += str(self.sqlerror)
return ret return ret
class PathError(Error): class PathError(Error):
"""Base class for path errors."""
pass pass
class PathNotFoundError(PathError): class PathNotFoundError(PathError):
"""A path with no file was required; found a file""" """A path with no file was required; found a file."""
pass pass
class PathFoundError(PathError): class PathFoundError(PathError):
"""A path with a file was required; found no file""" """A path with a file was required; found no file."""
pass pass
class RepositoryError(Error): class RepositoryError(Error):
"""Base class for repository errors."""
pass pass
class InvalidRepositoryError(RepositoryError): class InvalidRepositoryError(RepositoryError):
"""Invalid repository error."""
pass pass
class ScriptError(Error): class ScriptError(Error):
"""Base class for script errors."""
pass pass
class InvalidScriptError(ScriptError): class InvalidScriptError(ScriptError):
"""Invalid script error."""
pass pass
class InvalidVersionError(Error): class InvalidVersionError(Error):
"""Invalid version error."""
pass pass

View File

@ -1,9 +1,14 @@
"""
Code to generate a Python model from a database or differences
between a model and database.
# Code to generate a Python model from a database or differences between a model and database. Some of this is borrowed heavily from the AutoCode project at:
# Some of this is borrowed heavily from the AutoCode project at: http://code.google.com/p/sqlautocode/ http://code.google.com/p/sqlautocode/
"""
import sys import sys
import migrate, sqlalchemy import migrate
import sqlalchemy
HEADER = """ HEADER = """
@ -22,54 +27,66 @@ from sqlalchemy.ext import declarative
Base = declarative.declarative_base() Base = declarative.declarative_base()
""" """
class ModelGenerator(object): class ModelGenerator(object):
def __init__(self, diff, declarative=False): def __init__(self, diff, declarative=False):
self.diff = diff self.diff = diff
self.declarative = declarative self.declarative = declarative
dialectModule = sys.modules[self.diff.conn.dialect.__module__] # is there an easier way to get this? # is there an easier way to get this?
self.colTypeMappings = dict( (v,k) for k,v in dialectModule.colspecs.items() ) dialectModule = sys.modules[self.diff.conn.dialect.__module__]
self.colTypeMappings = dict((v, k) for k, v in \
dialectModule.colspecs.items())
def column_repr(self, col): def column_repr(self, col):
kwarg = [] kwarg = []
if col.key != col.name: kwarg.append('key') if col.key != col.name:
kwarg.append('key')
if col.primary_key: if col.primary_key:
col.primary_key = True # otherwise it dumps it as 1 col.primary_key = True # otherwise it dumps it as 1
kwarg.append('primary_key') kwarg.append('primary_key')
if not col.nullable: kwarg.append('nullable') if not col.nullable:
if col.onupdate: kwarg.append('onupdate') kwarg.append('nullable')
if col.onupdate:
kwarg.append('onupdate')
if col.default: if col.default:
if col.primary_key: if col.primary_key:
# I found that Postgres automatically creates a default value for the sequence, but let's not show that. # I found that PostgreSQL automatically creates a
# default value for the sequence, but let's not show
# that.
pass pass
else: else:
kwarg.append('default') kwarg.append('default')
ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg ) ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg)
name = col.name.encode('utf8') # crs: not sure if this is good idea, but it gets rid of extra u'' # crs: not sure if this is good idea, but it gets rid of extra
# u''
name = col.name.encode('utf8')
type = self.colTypeMappings.get(col.type.__class__, None) type = self.colTypeMappings.get(col.type.__class__, None)
if type: if type:
# Make the column type be an instance of this type. # Make the column type be an instance of this type.
type = type() type = type()
else: else:
# We must already be a model type, no need to map from the database-specific types. # We must already be a model type, no need to map from the
# database-specific types.
type = col.type type = col.type
data = {'name' : name, data = {
'type' : type, 'name': name,
'constraints' : ', '.join([repr(cn) for cn in col.constraints]), 'type': type,
'args' : ks and ks or '' 'constraints': ', '.join([repr(cn) for cn in col.constraints]),
} 'args': ks and ks or ''}
if data['constraints']: if data['constraints']:
if data['args']: data['args'] = ',' + data['args'] if data['args']:
data['args'] = ',' + data['args']
if data['constraints'] or data['args']: if data['constraints'] or data['args']:
data['maybeComma'] = ',' data['maybeComma'] = ','
else: else:
data['maybeComma'] = '' data['maybeComma'] = ''
commonStuff = " %(maybeComma)s %(constraints)s %(args)s)""" % data commonStuff = """ %(maybeComma)s %(constraints)s %(args)s)""" % data
commonStuff = commonStuff.strip() commonStuff = commonStuff.strip()
data['commonStuff'] = commonStuff data['commonStuff'] = commonStuff
if self.declarative: if self.declarative:
@ -86,14 +103,15 @@ class ModelGenerator(object):
for col in table.columns: for col in table.columns:
out.append(" %s" % self.column_repr(col)) out.append(" %s" % self.column_repr(col))
else: else:
out.append("%(table)s = Table('%(table)s', meta," % {'table': tableName}) out.append("%(table)s = Table('%(table)s', meta," % \
{'table': tableName})
for col in table.columns: for col in table.columns:
out.append(" %s," % self.column_repr(col)) out.append(" %s," % self.column_repr(col))
out.append(")") out.append(")")
return out return out
def toPython(self): def toPython(self):
''' Assume database is current and model is empty. ''' """Assume database is current and model is empty."""
out = [] out = []
if self.declarative: if self.declarative:
out.append(DECLARATIVE_HEADER) out.append(DECLARATIVE_HEADER)
@ -109,28 +127,30 @@ class ModelGenerator(object):
''' Assume model is most current and database is out-of-date. ''' ''' Assume model is most current and database is out-of-date. '''
decls = ['meta = MetaData(migrate_engine)'] decls = ['meta = MetaData(migrate_engine)']
for table in self.diff.tablesMissingInModel + self.diff.tablesMissingInDatabase: for table in self.diff.tablesMissingInModel + \
self.diff.tablesMissingInDatabase:
decls.extend(self.getTableDefn(table)) decls.extend(self.getTableDefn(table))
upgradeCommands, downgradeCommands = [], [] upgradeCommands, downgradeCommands = [], []
for table in self.diff.tablesMissingInModel: for table in self.diff.tablesMissingInModel:
tableName = table.name tableName = table.name
upgradeCommands.append("%(table)s.drop()" % {'table': tableName}) upgradeCommands.append("%(table)s.drop()" % {'table': tableName})
downgradeCommands.append("%(table)s.create()" % {'table': tableName}) downgradeCommands.append("%(table)s.create()" % \
{'table': tableName})
for table in self.diff.tablesMissingInDatabase: for table in self.diff.tablesMissingInDatabase:
tableName = table.name tableName = table.name
upgradeCommands.append("%(table)s.create()" % {'table': tableName}) upgradeCommands.append("%(table)s.create()" % {'table': tableName})
downgradeCommands.append("%(table)s.drop()" % {'table': tableName}) downgradeCommands.append("%(table)s.drop()" % {'table': tableName})
return ('\n'.join(decls), return (
'\n'.join(decls),
'\n'.join(['%s%s' % (indent, line) for line in upgradeCommands]), '\n'.join(['%s%s' % (indent, line) for line in upgradeCommands]),
'\n'.join(['%s%s' % (indent, line) for line in downgradeCommands]) '\n'.join(['%s%s' % (indent, line) for line in downgradeCommands]))
)
def applyModel(self): def applyModel(self):
''' Apply model to current database. ''' """Apply model to current database."""
# Yuck! We have to import from changeset to apply the
# Yuck! We have to import from changeset to apply the monkey-patch to allow column adding/dropping. # monkey-patch to allow column adding/dropping.
from migrate.changeset import schema from migrate.changeset import schema
def dbCanHandleThisChange(missingInDatabase, missingInModel, diffDecl): def dbCanHandleThisChange(missingInDatabase, missingInModel, diffDecl):
@ -152,8 +172,10 @@ class ModelGenerator(object):
modelTable = modelTable.tometadata(meta) modelTable = modelTable.tometadata(meta)
dbTable = self.diff.reflected_model.tables[modelTable.name] dbTable = self.diff.reflected_model.tables[modelTable.name]
tableName = modelTable.name tableName = modelTable.name
missingInDatabase, missingInModel, diffDecl = self.diff.colDiffs[tableName] missingInDatabase, missingInModel, diffDecl = \
if dbCanHandleThisChange(missingInDatabase, missingInModel, diffDecl): self.diff.colDiffs[tableName]
if dbCanHandleThisChange(missingInDatabase, missingInModel,
diffDecl):
for col in missingInDatabase: for col in missingInDatabase:
modelTable.columns[col.name].create() modelTable.columns[col.name].create()
for col in missingInModel: for col in missingInModel:
@ -161,25 +183,34 @@ class ModelGenerator(object):
for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl: for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl:
databaseCol.alter(modelCol) databaseCol.alter(modelCol)
else: else:
# Sqlite doesn't support drop column, so you have to do more: # Sqlite doesn't support drop column, so you have to
# create temp table, copy data to it, drop old table, create new table, copy data back. # do more: create temp table, copy data to it, drop
# old table, create new table, copy data back.
#
# I wonder if this is guaranteed to be unique?
tempName = '_temp_%s' % modelTable.name
tempName = '_temp_%s' % modelTable.name # I wonder if this is guaranteed to be unique?
def getCopyStatement(): def getCopyStatement():
preparer = self.diff.conn.engine.dialect.preparer preparer = self.diff.conn.engine.dialect.preparer
commonCols = [] commonCols = []
for modelCol in modelTable.columns: for modelCol in modelTable.columns:
if dbTable.columns.has_key(modelCol.name): if modelCol.name in dbTable.columns:
commonCols.append(modelCol.name) commonCols.append(modelCol.name)
commonColsStr = ', '.join(commonCols) commonColsStr = ', '.join(commonCols)
return 'INSERT INTO %s (%s) SELECT %s FROM %s' % (tableName, commonColsStr, commonColsStr, tempName) return 'INSERT INTO %s (%s) SELECT %s FROM %s' % \
(tableName, commonColsStr, commonColsStr, tempName)
# Move the data in one transaction, so that we don't leave the database in a nasty state. # Move the data in one transaction, so that we don't
# leave the database in a nasty state.
connection = self.diff.conn.connect() connection = self.diff.conn.connect()
trans = connection.begin() trans = connection.begin()
try: try:
connection.execute('CREATE TEMPORARY TABLE %s as SELECT * from %s' % (tempName, modelTable.name)) connection.execute(
modelTable.drop(bind=connection) # make sure the drop takes place inside our transaction with the bind parameter 'CREATE TEMPORARY TABLE %s as SELECT * from %s' % \
(tempName, modelTable.name))
# make sure the drop takes place inside our
# transaction with the bind parameter
modelTable.drop(bind=connection)
modelTable.create(bind=connection) modelTable.create(bind=connection)
connection.execute(getCopyStatement()) connection.execute(getCopyStatement())
connection.execute('DROP TABLE %s' % tempName) connection.execute('DROP TABLE %s' % tempName)
@ -187,4 +218,3 @@ class ModelGenerator(object):
except: except:
trans.rollback() trans.rollback()
raise raise

View File

@ -1,17 +1,22 @@
""" Script to migrate repository. This shouldn't use any other migrate """
modules, so that it can work in any version. """ Script to migrate repository from sqlalchemy <= 0.4.4 to the new
repository schema. This shouldn't use any other migrate modules, so
that it can work in any version.
"""
import os, os.path, sys import os
import os.path
import sys
def usage(): def usage():
"""Gives usage information.""" """Gives usage information."""
print '''Usage: %(prog)s repository-to-migrate print """Usage: %(prog)s repository-to-migrate
Upgrade your repository to the new flat format. Upgrade your repository to the new flat format.
NOTE: You should probably make a backup before running this. NOTE: You should probably make a backup before running this.
''' % {'prog': sys.argv[0]} """ % {'prog': sys.argv[0]}
sys.exit(1) sys.exit(1)
@ -27,7 +32,8 @@ def move_file(src, tgt):
print ' Moving file %s to %s' % (src, tgt) print ' Moving file %s to %s' % (src, tgt)
if os.path.exists(tgt): if os.path.exists(tgt):
raise Exception( raise Exception(
'Cannot move file %s because target %s already exists' % (src, tgt)) 'Cannot move file %s because target %s already exists' % \
(src, tgt))
os.rename(src, tgt) os.rename(src, tgt)
@ -43,7 +49,7 @@ def migrate_repository(repos):
versions = '%s/versions' % repos versions = '%s/versions' % repos
dirs = os.listdir(versions) dirs = os.listdir(versions)
# Only use int's in list. # Only use int's in list.
numdirs = [ int(dirname) for dirname in dirs if dirname.isdigit() ] numdirs = [int(dirname) for dirname in dirs if dirname.isdigit()]
numdirs.sort() # Sort list. numdirs.sort() # Sort list.
for dirname in numdirs: for dirname in numdirs:
origdir = '%s/%s' % (versions, dirname) origdir = '%s/%s' % (versions, dirname)
@ -51,7 +57,6 @@ def migrate_repository(repos):
files = os.listdir(origdir) files = os.listdir(origdir)
files.sort() files.sort()
for filename in files: for filename in files:
# Delete compiled Python files. # Delete compiled Python files.
if filename.endswith('.pyc') or filename.endswith('.pyo'): if filename.endswith('.pyc') or filename.endswith('.pyo'):
delete_file('%s/%s' % (origdir, filename)) delete_file('%s/%s' % (origdir, filename))
@ -91,4 +96,3 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -1,60 +1,72 @@
"""
A path/directory class.
"""
from migrate.versioning.base import * from migrate.versioning.base import *
from migrate.versioning.util import KeyedInstance from migrate.versioning.util import KeyedInstance
import os,shutil
from migrate.versioning import exceptions from migrate.versioning import exceptions
import os
import shutil
class Pathed(KeyedInstance): class Pathed(KeyedInstance):
"""A class associated with a path/directory tree """
A class associated with a path/directory tree.
Only one instance of this class may exist for a particular file; Only one instance of this class may exist for a particular file;
__new__ will return an existing instance if possible __new__ will return an existing instance if possible
""" """
parent=None parent=None
@classmethod @classmethod
def _key(cls,path): def _key(cls, path):
return str(path) return str(path)
def __init__(self,path): def __init__(self, path):
self.path=path self.path=path
if self.__class__.parent is not None: if self.__class__.parent is not None:
self._init_parent(path) self._init_parent(path)
def _init_parent(self,path): def _init_parent(self, path):
"""Try to initialize this object's parent, if it has one""" """Try to initialize this object's parent, if it has one"""
parent_path=self.__class__._parent_path(path) parent_path=self.__class__._parent_path(path)
self.parent=self.__class__.parent(parent_path) self.parent=self.__class__.parent(parent_path)
log.info("Getting parent %r:%r"%(self.__class__.parent,parent_path)) log.info("Getting parent %r:%r" % (self.__class__.parent, parent_path))
self.parent._init_child(path,self) self.parent._init_child(path, self)
def _init_child(self,child,path): def _init_child(self, child, path):
"""Run when a child of this object is initialized """Run when a child of this object is initialized.
Parameters: the child object; the path to this object (its parent)
Parameters: the child object; the path to this object (its
parent)
""" """
pass pass
@classmethod @classmethod
def _parent_path(cls,path): def _parent_path(cls, path):
"""Fetch the path of this object's parent from this object's path
""" """
# os.path.dirname(), but strip directories like files (like unix basename) Fetch the path of this object's parent from this object's path.
"""
# os.path.dirname(), but strip directories like files (like
# unix basename)
#
# Treat directories like files... # Treat directories like files...
if path[-1]=='/': if path[-1] == '/':
path=path[:-1] path=path[:-1]
ret = os.path.dirname(path) ret = os.path.dirname(path)
return ret return ret
@classmethod @classmethod
def require_notfound(cls,path): def require_notfound(cls, path):
"""Ensures a given path does not already exist""" """Ensures a given path does not already exist"""
if os.path.exists(path): if os.path.exists(path):
raise exceptions.PathFoundError(path) raise exceptions.PathFoundError(path)
@classmethod @classmethod
def require_found(cls,path): def require_found(cls, path):
"""Ensures a given path already exists""" """Ensures a given path already exists"""
if not os.path.exists(path): if not os.path.exists(path):
raise exceptions.PathNotFoundError(path) raise exceptions.PathNotFoundError(path)
def __str__(self): def __str__(self):
return self.path return self.path