1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | |
---|
4 | import os |
---|
5 | import re |
---|
6 | import shutil |
---|
7 | |
---|
8 | from migrate.versioning import exceptions, pathed, script |
---|
9 | |
---|
10 | |
---|
11 | class VerNum(object): |
---|
12 | """A version number""" |
---|
13 | |
---|
14 | _instances = dict() |
---|
15 | |
---|
16 | def __new__(cls, value): |
---|
17 | val = str(value) |
---|
18 | if val not in cls._instances: |
---|
19 | cls._instances[val] = super(VerNum, cls).__new__(cls) |
---|
20 | ret = cls._instances[val] |
---|
21 | return ret |
---|
22 | |
---|
23 | def __init__(self,value): |
---|
24 | self.value = str(int(value)) |
---|
25 | if self < 0: |
---|
26 | raise ValueError("Version number cannot be negative") |
---|
27 | |
---|
28 | def __add__(self, value): |
---|
29 | ret = int(self) + int(value) |
---|
30 | return VerNum(ret) |
---|
31 | |
---|
32 | def __sub__(self, value): |
---|
33 | return self + (int(value) * -1) |
---|
34 | |
---|
35 | def __cmp__(self, value): |
---|
36 | return int(self) - int(value) |
---|
37 | |
---|
38 | def __repr__(self): |
---|
39 | return "<VerNum(%s)>" % self.value |
---|
40 | |
---|
41 | def __str__(self): |
---|
42 | return str(self.value) |
---|
43 | |
---|
44 | def __int__(self): |
---|
45 | return int(self.value) |
---|
46 | |
---|
47 | |
---|
48 | class Collection(pathed.Pathed): |
---|
49 | """A collection of versioning scripts in a repository""" |
---|
50 | |
---|
51 | FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*') |
---|
52 | |
---|
53 | def __init__(self, path): |
---|
54 | """Collect current version scripts in repository""" |
---|
55 | super(Collection, self).__init__(path) |
---|
56 | |
---|
57 | # Create temporary list of files, allowing skipped version numbers. |
---|
58 | files = os.listdir(path) |
---|
59 | if '1' in files: |
---|
60 | # deprecation |
---|
61 | raise Exception('It looks like you have a repository in the old ' |
---|
62 | 'format (with directories for each version). ' |
---|
63 | 'Please convert repository before proceeding.') |
---|
64 | |
---|
65 | tempVersions = dict() |
---|
66 | for filename in files: |
---|
67 | match = self.FILENAME_WITH_VERSION.match(filename) |
---|
68 | if match: |
---|
69 | num = int(match.group(1)) |
---|
70 | tempVersions.setdefault(num, []).append(filename) |
---|
71 | else: |
---|
72 | pass # Must be a helper file or something, let's ignore it. |
---|
73 | |
---|
74 | # Create the versions member where the keys |
---|
75 | # are VerNum's and the values are Version's. |
---|
76 | self.versions = dict() |
---|
77 | for num, files in tempVersions.items(): |
---|
78 | self.versions[VerNum(num)] = Version(num, path, files) |
---|
79 | |
---|
80 | @property |
---|
81 | def latest(self): |
---|
82 | return max([VerNum(0)] + self.versions.keys()) |
---|
83 | |
---|
84 | def create_new_python_version(self, description, **k): |
---|
85 | """Create Python files for new version""" |
---|
86 | ver = self.latest + 1 |
---|
87 | extra = str_to_filename(description) |
---|
88 | |
---|
89 | if extra: |
---|
90 | if extra == '_': |
---|
91 | extra = '' |
---|
92 | elif not extra.startswith('_'): |
---|
93 | extra = '_%s' % extra |
---|
94 | |
---|
95 | filename = '%03d%s.py' % (ver, extra) |
---|
96 | filepath = self._version_path(filename) |
---|
97 | |
---|
98 | if os.path.exists(filepath): |
---|
99 | raise Exception('Script already exists: %s' % filepath) |
---|
100 | else: |
---|
101 | script.PythonScript.create(filepath) |
---|
102 | |
---|
103 | self.versions[ver] = Version(ver, self.path, [filename]) |
---|
104 | |
---|
105 | def create_new_sql_version(self, database, **k): |
---|
106 | """Create SQL files for new version""" |
---|
107 | ver = self.latest + 1 |
---|
108 | self.versions[ver] = Version(ver, self.path, []) |
---|
109 | |
---|
110 | # Create new files. |
---|
111 | for op in ('upgrade', 'downgrade'): |
---|
112 | filename = '%03d_%s_%s.sql' % (ver, database, op) |
---|
113 | filepath = self._version_path(filename) |
---|
114 | if os.path.exists(filepath): |
---|
115 | raise Exception('Script already exists: %s' % filepath) |
---|
116 | else: |
---|
117 | open(filepath, "w").close() |
---|
118 | self.versions[ver].add_script(filepath) |
---|
119 | |
---|
120 | def version(self, vernum=None): |
---|
121 | """Returns latest Version if vernum is not given. \ |
---|
122 | Otherwise, returns wanted version""" |
---|
123 | if vernum is None: |
---|
124 | vernum = self.latest |
---|
125 | return self.versions[VerNum(vernum)] |
---|
126 | |
---|
127 | @classmethod |
---|
128 | def clear(cls): |
---|
129 | super(Collection, cls).clear() |
---|
130 | |
---|
131 | def _version_path(self, ver): |
---|
132 | """Returns path of file in versions repository""" |
---|
133 | return os.path.join(self.path, str(ver)) |
---|
134 | |
---|
135 | |
---|
136 | class Version(object): |
---|
137 | """A single version in a collection """ |
---|
138 | |
---|
139 | def __init__(self, vernum, path, filelist): |
---|
140 | self.version = VerNum(vernum) |
---|
141 | |
---|
142 | # Collect scripts in this folder |
---|
143 | self.sql = dict() |
---|
144 | self.python = None |
---|
145 | |
---|
146 | for script in filelist: |
---|
147 | self.add_script(os.path.join(path, script)) |
---|
148 | |
---|
149 | def script(self, database=None, operation=None): |
---|
150 | """Returns SQL or Python Script""" |
---|
151 | for db in (database, 'default'): |
---|
152 | # Try to return a .sql script first |
---|
153 | try: |
---|
154 | return self.sql[db][operation] |
---|
155 | except KeyError: |
---|
156 | continue # No .sql script exists |
---|
157 | |
---|
158 | # TODO: maybe add force Python parameter? |
---|
159 | ret = self.python |
---|
160 | |
---|
161 | assert ret is not None |
---|
162 | return ret |
---|
163 | |
---|
164 | # deprecated? |
---|
165 | @classmethod |
---|
166 | def create(cls, path): |
---|
167 | os.mkdir(path) |
---|
168 | # create the version as a proper Python package |
---|
169 | initfile = os.path.join(path, "__init__.py") |
---|
170 | if not os.path.exists(initfile): |
---|
171 | # just touch the file |
---|
172 | open(initfile, "w").close() |
---|
173 | try: |
---|
174 | ret = cls(path) |
---|
175 | except: |
---|
176 | os.rmdir(path) |
---|
177 | raise |
---|
178 | return ret |
---|
179 | |
---|
180 | def add_script(self, path): |
---|
181 | """Add script to Collection/Version""" |
---|
182 | if path.endswith(Extensions.py): |
---|
183 | self._add_script_py(path) |
---|
184 | elif path.endswith(Extensions.sql): |
---|
185 | self._add_script_sql(path) |
---|
186 | |
---|
187 | SQL_FILENAME = re.compile(r'^(\d+)_([^_]+)_([^_]+).sql') |
---|
188 | |
---|
189 | def _add_script_sql(self, path): |
---|
190 | match = self.SQL_FILENAME.match(os.path.basename(path)) |
---|
191 | |
---|
192 | if match: |
---|
193 | version, dbms, op = match.group(1), match.group(2), match.group(3) |
---|
194 | else: |
---|
195 | raise exceptions.ScriptError("Invalid SQL script name %s" % path) |
---|
196 | |
---|
197 | # File the script into a dictionary |
---|
198 | self.sql.setdefault(dbms, {})[op] = script.SqlScript(path) |
---|
199 | |
---|
200 | def _add_script_py(self, path): |
---|
201 | if self.python is not None: |
---|
202 | raise Exception('You can only have one Python script per version,' |
---|
203 | ' but you have: %s and %s' % (self.python, path)) |
---|
204 | self.python = script.PythonScript(path) |
---|
205 | |
---|
206 | class Extensions: |
---|
207 | """A namespace for file extensions""" |
---|
208 | py = 'py' |
---|
209 | sql = 'sql' |
---|
210 | |
---|
211 | def str_to_filename(s): |
---|
212 | """Replaces spaces, (double and single) quotes |
---|
213 | and double underscores to underscores |
---|
214 | """ |
---|
215 | |
---|
216 | s = s.replace(' ', '_').replace('"', '_').replace("'", '_') |
---|
217 | while '__' in s: |
---|
218 | s = s.replace('__', '_') |
---|
219 | return s |
---|