repository.py
243 lines
| 7.9 KiB
| text/x-python
|
PythonLexer
r1 | """ | |||
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 rhodecode.lib.dbmigrate.migrate import exceptions | ||||
from rhodecode.lib.dbmigrate.migrate.versioning import version, pathed, cfgparse | ||||
from rhodecode.lib.dbmigrate.migrate.versioning.template import Template | ||||
from rhodecode.lib.dbmigrate.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): | ||||
r4988 | return iter(list(self.items())) | |||
r1 | ||||
def keys(self): | ||||
""" | ||||
In a series of upgrades x -> y, keys are version x. Sorted. | ||||
""" | ||||
r4988 | ret = list(super(Changeset, self).keys()) | |||
r1 | # Reverse order if downgrading | |||
ret.sort(reverse=(self.step < 1)) | ||||
return ret | ||||
def values(self): | ||||
r4988 | return [self[k] for k in list(self.keys())] | |||
r1 | ||||
def items(self): | ||||
r4988 | return list(zip(list(self.keys()), list(self.values()))) | |||
r1 | ||||
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): | ||||
r3061 | log.debug('Loading repository %s...', path) | |||
r1 | 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)) | ||||
r3061 | log.debug('Repository %s loaded successfully', path) | |||
log.debug('Config: %r', self.config.to_dict()) | ||||
r1 | ||||
@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 as 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', []) | ||||
options.setdefault('use_timestamp_numbering', False) | ||||
with open(os.path.join(tmpl_dir, cls._config)) as f: | ||||
tmpl = f.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) | ||||
with open(os.path.join(path, cls._config), 'w') as fd: | ||||
fd.write(config_text) | ||||
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`""" | ||||
k['use_timestamp_numbering'] = self.use_timestamp_numbering | ||||
self.versions.create_new_python_version(description, **k) | ||||
def create_script_sql(self, database, description, **k): | ||||
"""API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`""" | ||||
k['use_timestamp_numbering'] = self.use_timestamp_numbering | ||||
self.versions.create_new_sql_version(database, description, **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') | ||||
@property | ||||
def use_timestamp_numbering(self): | ||||
"""Returns use_timestamp_numbering specified in config""" | ||||
if self.config.has_option('db_settings', 'use_timestamp_numbering'): | ||||
return self.config.getboolean('db_settings', 'use_timestamp_numbering') | ||||
return False | ||||
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' | ||||
r4988 | versions = list(range(int(start) + range_mod, int(end) + range_mod, step)) | |||
r1 | 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)) | ||||
with open(mng_file) as f: | ||||
tmpl = f.read() | ||||
with open(file_, 'w') as fd: | ||||
fd.write(TempitaTemplate(tmpl).substitute(opts)) | ||||