comparison 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
comparison
equal deleted inserted replaced
832:634596f81cfd 833:9753e0907827
1 """
2 SQLAlchemy migrate repository management.
3 """
4 import os
5 import shutil
6 import string
7 import logging
8
9 from pkg_resources import resource_filename
10 from tempita import Template as TempitaTemplate
11
12 from migrate import exceptions
13 from migrate.versioning import version, pathed, cfgparse
14 from migrate.versioning.template import Template
15 from migrate.versioning.config import *
16
17
18 log = logging.getLogger(__name__)
19
20 class Changeset(dict):
21 """A collection of changes to be applied to a database.
22
23 Changesets are bound to a repository and manage a set of
24 scripts from that repository.
25
26 Behaves like a dict, for the most part. Keys are ordered based on step value.
27 """
28
29 def __init__(self, start, *changes, **k):
30 """
31 Give a start version; step must be explicitly stated.
32 """
33 self.step = k.pop('step', 1)
34 self.start = version.VerNum(start)
35 self.end = self.start
36 for change in changes:
37 self.add(change)
38
39 def __iter__(self):
40 return iter(self.items())
41
42 def keys(self):
43 """
44 In a series of upgrades x -> y, keys are version x. Sorted.
45 """
46 ret = super(Changeset, self).keys()
47 # Reverse order if downgrading
48 ret.sort(reverse=(self.step < 1))
49 return ret
50
51 def values(self):
52 return [self[k] for k in self.keys()]
53
54 def items(self):
55 return zip(self.keys(), self.values())
56
57 def add(self, change):
58 """Add new change to changeset"""
59 key = self.end
60 self.end += self.step
61 self[key] = change
62
63 def run(self, *p, **k):
64 """Run the changeset scripts"""
65 for version, script in self:
66 script.run(*p, **k)
67
68
69 class Repository(pathed.Pathed):
70 """A project's change script repository"""
71
72 _config = 'migrate.cfg'
73 _versions = 'versions'
74
75 def __init__(self, path):
76 log.debug('Loading repository %s...' % path)
77 self.verify(path)
78 super(Repository, self).__init__(path)
79 self.config = cfgparse.Config(os.path.join(self.path, self._config))
80 self.versions = version.Collection(os.path.join(self.path,
81 self._versions))
82 log.debug('Repository %s loaded successfully' % path)
83 log.debug('Config: %r' % self.config.to_dict())
84
85 @classmethod
86 def verify(cls, path):
87 """
88 Ensure the target path is a valid repository.
89
90 :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
91 """
92 # Ensure the existence of required files
93 try:
94 cls.require_found(path)
95 cls.require_found(os.path.join(path, cls._config))
96 cls.require_found(os.path.join(path, cls._versions))
97 except exceptions.PathNotFoundError, e:
98 raise exceptions.InvalidRepositoryError(path)
99
100 @classmethod
101 def prepare_config(cls, tmpl_dir, name, options=None):
102 """
103 Prepare a project configuration file for a new project.
104
105 :param tmpl_dir: Path to Repository template
106 :param config_file: Name of the config file in Repository template
107 :param name: Repository name
108 :type tmpl_dir: string
109 :type config_file: string
110 :type name: string
111 :returns: Populated config file
112 """
113 if options is None:
114 options = {}
115 options.setdefault('version_table', 'migrate_version')
116 options.setdefault('repository_id', name)
117 options.setdefault('required_dbs', [])
118
119 tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
120 ret = TempitaTemplate(tmpl).substitute(options)
121
122 # cleanup
123 del options['__template_name__']
124
125 return ret
126
127 @classmethod
128 def create(cls, path, name, **opts):
129 """Create a repository at a specified path"""
130 cls.require_notfound(path)
131 theme = opts.pop('templates_theme', None)
132 t_path = opts.pop('templates_path', None)
133
134 # Create repository
135 tmpl_dir = Template(t_path).get_repository(theme=theme)
136 shutil.copytree(tmpl_dir, path)
137
138 # Edit config defaults
139 config_text = cls.prepare_config(tmpl_dir, name, options=opts)
140 fd = open(os.path.join(path, cls._config), 'w')
141 fd.write(config_text)
142 fd.close()
143
144 opts['repository_name'] = name
145
146 # Create a management script
147 manager = os.path.join(path, 'manage.py')
148 Repository.create_manage_file(manager, templates_theme=theme,
149 templates_path=t_path, **opts)
150
151 return cls(path)
152
153 def create_script(self, description, **k):
154 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
155 self.versions.create_new_python_version(description, **k)
156
157 def create_script_sql(self, database, **k):
158 """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
159 self.versions.create_new_sql_version(database, **k)
160
161 @property
162 def latest(self):
163 """API to :attr:`migrate.versioning.version.Collection.latest`"""
164 return self.versions.latest
165
166 @property
167 def version_table(self):
168 """Returns version_table name specified in config"""
169 return self.config.get('db_settings', 'version_table')
170
171 @property
172 def id(self):
173 """Returns repository id specified in config"""
174 return self.config.get('db_settings', 'repository_id')
175
176 def version(self, *p, **k):
177 """API to :attr:`migrate.versioning.version.Collection.version`"""
178 return self.versions.version(*p, **k)
179
180 @classmethod
181 def clear(cls):
182 # TODO: deletes repo
183 super(Repository, cls).clear()
184 version.Collection.clear()
185
186 def changeset(self, database, start, end=None):
187 """Create a changeset to migrate this database from ver. start to end/latest.
188
189 :param database: name of database to generate changeset
190 :param start: version to start at
191 :param end: version to end at (latest if None given)
192 :type database: string
193 :type start: int
194 :type end: int
195 :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
196 """
197 start = version.VerNum(start)
198
199 if end is None:
200 end = self.latest
201 else:
202 end = version.VerNum(end)
203
204 if start <= end:
205 step = 1
206 range_mod = 1
207 op = 'upgrade'
208 else:
209 step = -1
210 range_mod = 0
211 op = 'downgrade'
212
213 versions = range(start + range_mod, end + range_mod, step)
214 changes = [self.version(v).script(database, op) for v in versions]
215 ret = Changeset(start, step=step, *changes)
216 return ret
217
218 @classmethod
219 def create_manage_file(cls, file_, **opts):
220 """Create a project management script (manage.py)
221
222 :param file_: Destination file to be written
223 :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
224 """
225 mng_file = Template(opts.pop('templates_path', None))\
226 .get_manage(theme=opts.pop('templates_theme', None))
227
228 tmpl = open(mng_file).read()
229 fd = open(file_, 'w')
230 fd.write(TempitaTemplate(tmpl).substitute(opts))
231 fd.close()