1 | """ |
---|
2 | SQLAlchemy migrate repository management. |
---|
3 | """ |
---|
4 | import os |
---|
5 | import shutil |
---|
6 | import string |
---|
7 | from pkg_resources import resource_string, resource_filename |
---|
8 | |
---|
9 | from migrate.versioning import exceptions, script, version, pathed, cfgparse |
---|
10 | from migrate.versioning.template import template |
---|
11 | from migrate.versioning.base import * |
---|
12 | |
---|
13 | |
---|
14 | class Changeset(dict): |
---|
15 | """A collection of changes to be applied to a database. |
---|
16 | |
---|
17 | Changesets are bound to a repository and manage a set of logsql |
---|
18 | scripts from that repository. |
---|
19 | |
---|
20 | Behaves like a dict, for the most part. Keys are ordered based on |
---|
21 | start/end. |
---|
22 | """ |
---|
23 | |
---|
24 | def __init__(self, start, *changes, **k): |
---|
25 | """ |
---|
26 | Give a start version; step must be explicitly stated. |
---|
27 | """ |
---|
28 | self.step = k.pop('step', 1) |
---|
29 | self.start = version.VerNum(start) |
---|
30 | self.end = self.start |
---|
31 | for change in changes: |
---|
32 | self.add(change) |
---|
33 | |
---|
34 | def __iter__(self): |
---|
35 | return iter(self.items()) |
---|
36 | |
---|
37 | def keys(self): |
---|
38 | """ |
---|
39 | In a series of upgrades x -> y, keys are version x. Sorted. |
---|
40 | """ |
---|
41 | ret = super(Changeset, self).keys() |
---|
42 | # Reverse order if downgrading |
---|
43 | ret.sort(reverse=(self.step < 1)) |
---|
44 | return ret |
---|
45 | |
---|
46 | def values(self): |
---|
47 | return [self[k] for k in self.keys()] |
---|
48 | |
---|
49 | def items(self): |
---|
50 | return zip(self.keys(), self.values()) |
---|
51 | |
---|
52 | def add(self, change): |
---|
53 | key = self.end |
---|
54 | self.end += self.step |
---|
55 | self[key] = change |
---|
56 | |
---|
57 | def run(self, *p, **k): |
---|
58 | for version, script in self: |
---|
59 | script.run(*p, **k) |
---|
60 | |
---|
61 | |
---|
62 | class Repository(pathed.Pathed): |
---|
63 | """A project's change script repository""" |
---|
64 | _config = 'migrate.cfg' |
---|
65 | _versions = 'versions' |
---|
66 | |
---|
67 | def __init__(self, path): |
---|
68 | log.info('Loading repository %s...' % path) |
---|
69 | self.verify(path) |
---|
70 | super(Repository, self).__init__(path) |
---|
71 | self.config=cfgparse.Config(os.path.join(self.path, self._config)) |
---|
72 | self.versions=version.Collection(os.path.join(self.path, |
---|
73 | self._versions)) |
---|
74 | log.info('Repository %s loaded successfully' % path) |
---|
75 | log.debug('Config: %r' % self.config.to_dict()) |
---|
76 | |
---|
77 | @classmethod |
---|
78 | def verify(cls, path): |
---|
79 | """ |
---|
80 | Ensure the target path is a valid repository. |
---|
81 | |
---|
82 | :raises: :exc:`InvalidRepositoryError` if not valid |
---|
83 | """ |
---|
84 | # Ensure the existance of required files |
---|
85 | try: |
---|
86 | cls.require_found(path) |
---|
87 | cls.require_found(os.path.join(path, cls._config)) |
---|
88 | cls.require_found(os.path.join(path, cls._versions)) |
---|
89 | except exceptions.PathNotFoundError, e: |
---|
90 | raise exceptions.InvalidRepositoryError(path) |
---|
91 | |
---|
92 | @classmethod |
---|
93 | def prepare_config(cls, pkg, rsrc, name, **opts): |
---|
94 | """ |
---|
95 | Prepare a project configuration file for a new project. |
---|
96 | """ |
---|
97 | # Prepare opts |
---|
98 | defaults=dict( |
---|
99 | version_table='migrate_version', |
---|
100 | repository_id=name, |
---|
101 | required_dbs=[], ) |
---|
102 | for key, val in defaults.iteritems(): |
---|
103 | if (key not in opts) or (opts[key] is None): |
---|
104 | opts[key]=val |
---|
105 | |
---|
106 | tmpl = resource_string(pkg, rsrc) |
---|
107 | ret = string.Template(tmpl).substitute(opts) |
---|
108 | return ret |
---|
109 | |
---|
110 | @classmethod |
---|
111 | def create(cls, path, name, **opts): |
---|
112 | """Create a repository at a specified path""" |
---|
113 | cls.require_notfound(path) |
---|
114 | |
---|
115 | pkg, rsrc = template.get_repository(as_pkg=True) |
---|
116 | tmplpkg = '.'.join((pkg, rsrc)) |
---|
117 | tmplfile = resource_filename(pkg, rsrc) |
---|
118 | config_text = cls.prepare_config(tmplpkg, cls._config, name, **opts) |
---|
119 | # Create repository |
---|
120 | try: |
---|
121 | shutil.copytree(tmplfile, path) |
---|
122 | # Edit config defaults |
---|
123 | fd = open(os.path.join(path, cls._config), 'w') |
---|
124 | fd.write(config_text) |
---|
125 | fd.close() |
---|
126 | # Create a management script |
---|
127 | manager = os.path.join(path, 'manage.py') |
---|
128 | manage(manager, repository=path) |
---|
129 | except: |
---|
130 | log.error("There was an error creating your repository") |
---|
131 | return cls(path) |
---|
132 | |
---|
133 | def create_script(self, description, **k): |
---|
134 | self.versions.create_new_python_version(description, **k) |
---|
135 | |
---|
136 | def create_script_sql(self, database, **k): |
---|
137 | self.versions.create_new_sql_version(database, **k) |
---|
138 | |
---|
139 | latest=property(lambda self: self.versions.latest) |
---|
140 | version_table=property(lambda self: self.config.get('db_settings', |
---|
141 | 'version_table')) |
---|
142 | id=property(lambda self: self.config.get('db_settings', 'repository_id')) |
---|
143 | |
---|
144 | def version(self, *p, **k): |
---|
145 | return self.versions.version(*p, **k) |
---|
146 | |
---|
147 | @classmethod |
---|
148 | def clear(cls): |
---|
149 | super(Repository, cls).clear() |
---|
150 | version.Collection.clear() |
---|
151 | |
---|
152 | def changeset(self, database, start, end=None): |
---|
153 | """ |
---|
154 | Create a changeset to migrate this dbms from ver. start to end/latest. |
---|
155 | """ |
---|
156 | start = version.VerNum(start) |
---|
157 | if end is None: |
---|
158 | end = self.latest |
---|
159 | else: |
---|
160 | end = version.VerNum(end) |
---|
161 | if start <= end: |
---|
162 | step = 1 |
---|
163 | range_mod = 1 |
---|
164 | op = 'upgrade' |
---|
165 | else: |
---|
166 | step = -1 |
---|
167 | range_mod = 0 |
---|
168 | op = 'downgrade' |
---|
169 | versions = range(start+range_mod, end+range_mod, step) |
---|
170 | changes = [self.version(v).script(database, op) for v in versions] |
---|
171 | ret = Changeset(start, step=step, *changes) |
---|
172 | return ret |
---|
173 | |
---|
174 | |
---|
175 | def manage(file, **opts): |
---|
176 | """Create a project management script""" |
---|
177 | pkg, rsrc = template.manage(as_pkg=True) |
---|
178 | tmpl = resource_string(pkg, rsrc) |
---|
179 | vars = ",".join(["%s='%s'" % vars for vars in opts.iteritems()]) |
---|
180 | result = tmpl%dict(defaults=vars) |
---|
181 | |
---|
182 | fd = open(file, 'w') |
---|
183 | fd.write(result) |
---|
184 | fd.close() |
---|