diff rhodecode/lib/dbmigrate/migrate/versioning/repository.py @ 833:9753e0907827 beta

added dbmigrate package, added model changes moved out upgrade db command to that package
author Marcin Kuzminski <marcin@python-works.com>
date Sat, 11 Dec 2010 01:54:12 +0100
parents
children 08d2dcd71666
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/lib/dbmigrate/migrate/versioning/repository.py	Sat Dec 11 01:54:12 2010 +0100
@@ -0,0 +1,231 @@
+"""
+   SQLAlchemy migrate repository management.
+"""
+import os
+import shutil
+import string
+import logging
+
+from pkg_resources import resource_filename
+from tempita import Template as TempitaTemplate
+
+from migrate import exceptions
+from migrate.versioning import version, pathed, cfgparse
+from migrate.versioning.template import Template
+from migrate.versioning.config import *
+
+
+log = logging.getLogger(__name__)
+
+class Changeset(dict):
+    """A collection of changes to be applied to a database.
+
+    Changesets are bound to a repository and manage a set of
+    scripts from that repository.
+
+    Behaves like a dict, for the most part. Keys are ordered based on step value.
+    """
+
+    def __init__(self, start, *changes, **k):
+        """
+        Give a start version; step must be explicitly stated.
+        """
+        self.step = k.pop('step', 1)
+        self.start = version.VerNum(start)
+        self.end = self.start
+        for change in changes:
+            self.add(change)
+
+    def __iter__(self):
+        return iter(self.items())
+
+    def keys(self):
+        """
+        In a series of upgrades x -> y, keys are version x. Sorted.
+        """
+        ret = super(Changeset, self).keys()
+        # Reverse order if downgrading
+        ret.sort(reverse=(self.step < 1))
+        return ret
+
+    def values(self):
+        return [self[k] for k in self.keys()]
+
+    def items(self):
+        return zip(self.keys(), self.values())
+
+    def add(self, change):
+        """Add new change to changeset"""
+        key = self.end
+        self.end += self.step
+        self[key] = change
+
+    def run(self, *p, **k):
+        """Run the changeset scripts"""
+        for version, script in self:
+            script.run(*p, **k)
+
+
+class Repository(pathed.Pathed):
+    """A project's change script repository"""
+
+    _config = 'migrate.cfg'
+    _versions = 'versions'
+
+    def __init__(self, path):
+        log.debug('Loading repository %s...' % path)
+        self.verify(path)
+        super(Repository, self).__init__(path)
+        self.config = cfgparse.Config(os.path.join(self.path, self._config))
+        self.versions = version.Collection(os.path.join(self.path,
+                                                      self._versions))
+        log.debug('Repository %s loaded successfully' % path)
+        log.debug('Config: %r' % self.config.to_dict())
+
+    @classmethod
+    def verify(cls, path):
+        """
+        Ensure the target path is a valid repository.
+
+        :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
+        """
+        # Ensure the existence of required files
+        try:
+            cls.require_found(path)
+            cls.require_found(os.path.join(path, cls._config))
+            cls.require_found(os.path.join(path, cls._versions))
+        except exceptions.PathNotFoundError, e:
+            raise exceptions.InvalidRepositoryError(path)
+
+    @classmethod
+    def prepare_config(cls, tmpl_dir, name, options=None):
+        """
+        Prepare a project configuration file for a new project.
+
+        :param tmpl_dir: Path to Repository template
+        :param config_file: Name of the config file in Repository template
+        :param name: Repository name
+        :type tmpl_dir: string
+        :type config_file: string
+        :type name: string
+        :returns: Populated config file
+        """
+        if options is None:
+            options = {}
+        options.setdefault('version_table', 'migrate_version')
+        options.setdefault('repository_id', name)
+        options.setdefault('required_dbs', [])
+
+        tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
+        ret = TempitaTemplate(tmpl).substitute(options)
+
+        # cleanup
+        del options['__template_name__']
+
+        return ret
+
+    @classmethod
+    def create(cls, path, name, **opts):
+        """Create a repository at a specified path"""
+        cls.require_notfound(path)
+        theme = opts.pop('templates_theme', None)
+        t_path = opts.pop('templates_path', None)
+
+        # Create repository
+        tmpl_dir = Template(t_path).get_repository(theme=theme)
+        shutil.copytree(tmpl_dir, path)
+
+        # Edit config defaults
+        config_text = cls.prepare_config(tmpl_dir, name, options=opts)
+        fd = open(os.path.join(path, cls._config), 'w')
+        fd.write(config_text)
+        fd.close()
+
+        opts['repository_name'] = name
+
+        # Create a management script
+        manager = os.path.join(path, 'manage.py')
+        Repository.create_manage_file(manager, templates_theme=theme,
+            templates_path=t_path, **opts)
+
+        return cls(path)
+
+    def create_script(self, description, **k):
+        """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
+        self.versions.create_new_python_version(description, **k)
+
+    def create_script_sql(self, database, **k):
+        """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
+        self.versions.create_new_sql_version(database, **k)
+
+    @property
+    def latest(self):
+        """API to :attr:`migrate.versioning.version.Collection.latest`"""
+        return self.versions.latest
+
+    @property
+    def version_table(self):
+        """Returns version_table name specified in config"""
+        return self.config.get('db_settings', 'version_table')
+
+    @property
+    def id(self):
+        """Returns repository id specified in config"""
+        return self.config.get('db_settings', 'repository_id')
+
+    def version(self, *p, **k):
+        """API to :attr:`migrate.versioning.version.Collection.version`"""
+        return self.versions.version(*p, **k)
+
+    @classmethod
+    def clear(cls):
+        # TODO: deletes repo
+        super(Repository, cls).clear()
+        version.Collection.clear()
+
+    def changeset(self, database, start, end=None):
+        """Create a changeset to migrate this database from ver. start to end/latest.
+
+        :param database: name of database to generate changeset
+        :param start: version to start at
+        :param end: version to end at (latest if None given)
+        :type database: string
+        :type start: int
+        :type end: int
+        :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
+        """
+        start = version.VerNum(start)
+
+        if end is None:
+            end = self.latest
+        else:
+            end = version.VerNum(end)
+
+        if start <= end:
+            step = 1
+            range_mod = 1
+            op = 'upgrade'
+        else:
+            step = -1
+            range_mod = 0
+            op = 'downgrade'
+
+        versions = range(start + range_mod, end + range_mod, step)
+        changes = [self.version(v).script(database, op) for v in versions]
+        ret = Changeset(start, step=step, *changes)
+        return ret
+
+    @classmethod
+    def create_manage_file(cls, file_, **opts):
+        """Create a project management script (manage.py)
+        
+        :param file_: Destination file to be written
+        :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
+        """
+        mng_file = Template(opts.pop('templates_path', None))\
+            .get_manage(theme=opts.pop('templates_theme', None))
+
+        tmpl = open(mng_file).read()
+        fd = open(file_, 'w')
+        fd.write(TempitaTemplate(tmpl).substitute(opts))
+        fd.close()