sqlobject 3.13.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. sqlobject/__init__.py +15 -0
  2. sqlobject/__version__.py +8 -0
  3. sqlobject/boundattributes.py +124 -0
  4. sqlobject/cache.py +376 -0
  5. sqlobject/classregistry.py +138 -0
  6. sqlobject/col.py +2187 -0
  7. sqlobject/compat.py +59 -0
  8. sqlobject/conftest.py +64 -0
  9. sqlobject/constraints.py +77 -0
  10. sqlobject/converters.py +278 -0
  11. sqlobject/dbconnection.py +1137 -0
  12. sqlobject/dberrors.py +54 -0
  13. sqlobject/declarative.py +211 -0
  14. sqlobject/events.py +365 -0
  15. sqlobject/firebird/__init__.py +8 -0
  16. sqlobject/firebird/firebirdconnection.py +463 -0
  17. sqlobject/include/__init__.py +1 -0
  18. sqlobject/include/hashcol.py +109 -0
  19. sqlobject/include/tests/__init__.py +1 -0
  20. sqlobject/include/tests/test_hashcol.py +84 -0
  21. sqlobject/index.py +189 -0
  22. sqlobject/inheritance/__init__.py +551 -0
  23. sqlobject/inheritance/iteration.py +106 -0
  24. sqlobject/inheritance/tests/__init__.py +1 -0
  25. sqlobject/inheritance/tests/test_aggregates.py +23 -0
  26. sqlobject/inheritance/tests/test_asdict.py +48 -0
  27. sqlobject/inheritance/tests/test_deep_inheritance.py +114 -0
  28. sqlobject/inheritance/tests/test_destroy_cascade.py +23 -0
  29. sqlobject/inheritance/tests/test_foreignKey.py +94 -0
  30. sqlobject/inheritance/tests/test_indexes.py +42 -0
  31. sqlobject/inheritance/tests/test_inheritance.py +164 -0
  32. sqlobject/inheritance/tests/test_inheritance_tree.py +50 -0
  33. sqlobject/joins.py +656 -0
  34. sqlobject/main.py +1880 -0
  35. sqlobject/manager/__init__.py +1 -0
  36. sqlobject/manager/command.py +1309 -0
  37. sqlobject/maxdb/__init__.py +8 -0
  38. sqlobject/maxdb/maxdbconnection.py +320 -0
  39. sqlobject/maxdb/readme.txt +27 -0
  40. sqlobject/mssql/__init__.py +8 -0
  41. sqlobject/mssql/mssqlconnection.py +417 -0
  42. sqlobject/mysql/__init__.py +8 -0
  43. sqlobject/mysql/mysqlconnection.py +562 -0
  44. sqlobject/postgres/__init__.py +8 -0
  45. sqlobject/postgres/pgconnection.py +619 -0
  46. sqlobject/sqlbuilder.py +1655 -0
  47. sqlobject/sqlite/__init__.py +8 -0
  48. sqlobject/sqlite/sqliteconnection.py +418 -0
  49. sqlobject/sresults.py +377 -0
  50. sqlobject/styles.py +174 -0
  51. sqlobject/sybase/__init__.py +8 -0
  52. sqlobject/sybase/sybaseconnection.py +183 -0
  53. sqlobject/tests/__init__.py +1 -0
  54. sqlobject/tests/dbtest.py +363 -0
  55. sqlobject/tests/test_ForeignKey.py +133 -0
  56. sqlobject/tests/test_ForeignKey_cascade.py +150 -0
  57. sqlobject/tests/test_NoneValuedResultItem.py +33 -0
  58. sqlobject/tests/test_SQLMultipleJoin.py +65 -0
  59. sqlobject/tests/test_SQLRelatedJoin.py +114 -0
  60. sqlobject/tests/test_SingleJoin.py +30 -0
  61. sqlobject/tests/test_aggregates.py +75 -0
  62. sqlobject/tests/test_aliases.py +49 -0
  63. sqlobject/tests/test_asdict.py +18 -0
  64. sqlobject/tests/test_auto.py +218 -0
  65. sqlobject/tests/test_basic.py +359 -0
  66. sqlobject/tests/test_blob.py +34 -0
  67. sqlobject/tests/test_boundattributes.py +67 -0
  68. sqlobject/tests/test_cache.py +57 -0
  69. sqlobject/tests/test_class_hash.py +22 -0
  70. sqlobject/tests/test_columns_order.py +18 -0
  71. sqlobject/tests/test_combining_joins.py +47 -0
  72. sqlobject/tests/test_comparison.py +24 -0
  73. sqlobject/tests/test_compat.py +10 -0
  74. sqlobject/tests/test_complex_sorting.py +77 -0
  75. sqlobject/tests/test_constraints.py +43 -0
  76. sqlobject/tests/test_converters.py +276 -0
  77. sqlobject/tests/test_create_drop.py +29 -0
  78. sqlobject/tests/test_csvexport.py +101 -0
  79. sqlobject/tests/test_csvimport.py +21 -0
  80. sqlobject/tests/test_cyclic_reference.py +55 -0
  81. sqlobject/tests/test_datetime.py +171 -0
  82. sqlobject/tests/test_dbconnection.py +17 -0
  83. sqlobject/tests/test_decimal.py +63 -0
  84. sqlobject/tests/test_declarative.py +83 -0
  85. sqlobject/tests/test_default_style.py +65 -0
  86. sqlobject/tests/test_delete.py +74 -0
  87. sqlobject/tests/test_distinct.py +36 -0
  88. sqlobject/tests/test_empty.py +22 -0
  89. sqlobject/tests/test_enum.py +75 -0
  90. sqlobject/tests/test_events.py +142 -0
  91. sqlobject/tests/test_exceptions.py +40 -0
  92. sqlobject/tests/test_expire.py +29 -0
  93. sqlobject/tests/test_groupBy.py +45 -0
  94. sqlobject/tests/test_identity.py +29 -0
  95. sqlobject/tests/test_indexes.py +100 -0
  96. sqlobject/tests/test_inheritance.py +35 -0
  97. sqlobject/tests/test_joins.py +160 -0
  98. sqlobject/tests/test_joins_conditional.py +143 -0
  99. sqlobject/tests/test_jsonbcol.py +53 -0
  100. sqlobject/tests/test_jsoncol.py +56 -0
  101. sqlobject/tests/test_lazy.py +165 -0
  102. sqlobject/tests/test_md5.py +13 -0
  103. sqlobject/tests/test_mysql.py +50 -0
  104. sqlobject/tests/test_new_joins.py +165 -0
  105. sqlobject/tests/test_parse_uri.py +147 -0
  106. sqlobject/tests/test_paste.py +92 -0
  107. sqlobject/tests/test_perConnection.py +20 -0
  108. sqlobject/tests/test_pickle.py +42 -0
  109. sqlobject/tests/test_picklecol.py +44 -0
  110. sqlobject/tests/test_postgres.py +82 -0
  111. sqlobject/tests/test_reparent_sqlmeta.py +33 -0
  112. sqlobject/tests/test_schema.py +30 -0
  113. sqlobject/tests/test_select.py +218 -0
  114. sqlobject/tests/test_select_through.py +63 -0
  115. sqlobject/tests/test_setters.py +23 -0
  116. sqlobject/tests/test_slice.py +59 -0
  117. sqlobject/tests/test_sorting.py +80 -0
  118. sqlobject/tests/test_sqlbuilder.py +134 -0
  119. sqlobject/tests/test_sqlbuilder_dbspecific.py +66 -0
  120. sqlobject/tests/test_sqlbuilder_importproxy.py +55 -0
  121. sqlobject/tests/test_sqlbuilder_joins_instances.py +90 -0
  122. sqlobject/tests/test_sqlite.py +160 -0
  123. sqlobject/tests/test_sqlmeta_idName.py +52 -0
  124. sqlobject/tests/test_sqlobject_admin.py +38 -0
  125. sqlobject/tests/test_string_id.py +79 -0
  126. sqlobject/tests/test_style.py +36 -0
  127. sqlobject/tests/test_subqueries.py +142 -0
  128. sqlobject/tests/test_transactions.py +122 -0
  129. sqlobject/tests/test_unicode.py +84 -0
  130. sqlobject/tests/test_uuidcol.py +40 -0
  131. sqlobject/tests/test_validation.py +125 -0
  132. sqlobject/tests/test_views.py +159 -0
  133. sqlobject/util/__init__.py +1 -0
  134. sqlobject/util/csvexport.py +210 -0
  135. sqlobject/util/csvimport.py +366 -0
  136. sqlobject/util/moduleloader.py +35 -0
  137. sqlobject/util/threadinglocal.py +6 -0
  138. sqlobject/versioning/__init__.py +120 -0
  139. sqlobject/versioning/test/__init__.py +0 -0
  140. sqlobject/versioning/test/test_version.py +183 -0
  141. sqlobject/views.py +151 -0
  142. sqlobject/wsgi_middleware.py +109 -0
  143. sqlobject-3.13.1.data/scripts/sqlobject-admin +36 -0
  144. sqlobject-3.13.1.data/scripts/sqlobject-convertOldURI +19 -0
  145. sqlobject-3.13.1.dist-info/METADATA +177 -0
  146. sqlobject-3.13.1.dist-info/RECORD +149 -0
  147. sqlobject-3.13.1.dist-info/WHEEL +6 -0
  148. sqlobject-3.13.1.dist-info/entry_points.txt +4 -0
  149. sqlobject-3.13.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1309 @@
1
+ #!/usr/bin/env python
2
+ from __future__ import print_function
3
+
4
+ import fnmatch
5
+ import optparse
6
+ import os
7
+ import re
8
+ import sys
9
+ import textwrap
10
+ import time
11
+ import warnings
12
+
13
+ try:
14
+ from paste.deploy import appconfig
15
+ except ImportError:
16
+ appconfig = None
17
+
18
+ import sqlobject
19
+ from sqlobject import col
20
+ from sqlobject.classregistry import findClass
21
+ from sqlobject.declarative import DeclarativeMeta
22
+ from sqlobject.util import moduleloader
23
+ from sqlobject.compat import PY2, with_metaclass, string_type
24
+
25
+ # It's not very unsafe to use tempnam like we are doing:
26
+ warnings.filterwarnings(
27
+ 'ignore', 'tempnam is a potential security risk.*',
28
+ RuntimeWarning, '.*command', 28)
29
+
30
+
31
+ if PY2:
32
+ # noqa for flake8 and python 3
33
+ input = raw_input # noqa
34
+
35
+
36
+ def nowarning_tempnam(*args, **kw):
37
+ return os.tempnam(*args, **kw)
38
+
39
+
40
+ class SQLObjectVersionTable(sqlobject.SQLObject):
41
+ """
42
+ This table is used to store information about the database and
43
+ its version (used with record and update commands).
44
+ """
45
+ class sqlmeta:
46
+ table = 'sqlobject_db_version'
47
+ version = col.StringCol()
48
+ updated = col.DateTimeCol(default=col.DateTimeCol.now)
49
+
50
+
51
+ def db_differences(soClass, conn):
52
+ """
53
+ Returns the differences between a class and the table in a
54
+ connection. Returns [] if no differences are found. This
55
+ function does the best it can; it can miss many differences.
56
+ """
57
+ # @@: Repeats a lot from CommandStatus.command, but it's hard
58
+ # to actually factor out the display logic. Or I'm too lazy
59
+ # to do so.
60
+ diffs = []
61
+ if not conn.tableExists(soClass.sqlmeta.table):
62
+ if soClass.sqlmeta.columns:
63
+ diffs.append('Does not exist in database')
64
+ else:
65
+ try:
66
+ columns = conn.columnsFromSchema(soClass.sqlmeta.table,
67
+ soClass)
68
+ except AttributeError:
69
+ # Database does not support reading columns
70
+ pass
71
+ else:
72
+ existing = {}
73
+ for _col in columns:
74
+ _col = _col.withClass(soClass)
75
+ existing[_col.dbName] = _col
76
+ missing = {}
77
+ for _col in soClass.sqlmeta.columnList:
78
+ if _col.dbName in existing:
79
+ del existing[_col.dbName]
80
+ else:
81
+ missing[_col.dbName] = _col
82
+ for _col in existing.values():
83
+ diffs.append('Database has extra column: %s' % _col.dbName)
84
+ for _col in missing.values():
85
+ diffs.append('Database missing column: %s' % _col.dbName)
86
+ return diffs
87
+
88
+
89
+ class CommandRunner(object):
90
+
91
+ def __init__(self):
92
+ self.commands = {}
93
+ self.command_aliases = {}
94
+
95
+ def run(self, argv):
96
+ invoked_as = argv[0]
97
+ args = argv[1:]
98
+ for i in range(len(args)):
99
+ if not args[i].startswith('-'):
100
+ # this must be a command
101
+ command = args[i].lower()
102
+ del args[i]
103
+ break
104
+ else:
105
+ # no command found
106
+ self.invalid('No COMMAND given (try "%s help")'
107
+ % os.path.basename(invoked_as))
108
+ real_command = self.command_aliases.get(command, command)
109
+ if real_command not in self.commands.keys():
110
+ self.invalid('COMMAND %s unknown' % command)
111
+ runner = self.commands[real_command](
112
+ invoked_as, command, args, self)
113
+ runner.run()
114
+
115
+ def register(self, command):
116
+ name = command.name
117
+ self.commands[name] = command
118
+ for alias in command.aliases:
119
+ self.command_aliases[alias] = name
120
+
121
+ def invalid(self, msg, code=2):
122
+ print(msg)
123
+ sys.exit(code)
124
+
125
+ the_runner = CommandRunner()
126
+ register = the_runner.register
127
+
128
+
129
+ def standard_parser(connection=True, simulate=True,
130
+ interactive=False, find_modules=True):
131
+ parser = optparse.OptionParser()
132
+ parser.add_option('-v', '--verbose',
133
+ help='Be verbose (multiple times for more verbosity)',
134
+ action='count',
135
+ dest='verbose',
136
+ default=0)
137
+ if simulate:
138
+ parser.add_option('-n', '--simulate',
139
+ help="Don't actually do anything (implies -v)",
140
+ action='store_true',
141
+ dest='simulate')
142
+ if connection:
143
+ parser.add_option('-c', '--connection',
144
+ help="The database connection URI",
145
+ metavar='URI',
146
+ dest='connection_uri')
147
+ parser.add_option('-f', '--config-file',
148
+ help="The Paste config file "
149
+ "that contains the database URI (in the database key)",
150
+ metavar="FILE",
151
+ dest="config_file")
152
+ if find_modules:
153
+ parser.add_option('-m', '--module',
154
+ help="Module in which to find SQLObject classes",
155
+ action='append',
156
+ metavar='MODULE',
157
+ dest='modules',
158
+ default=[])
159
+ parser.add_option('-p', '--package',
160
+ help="Package to search for SQLObject classes",
161
+ action="append",
162
+ metavar="PACKAGE",
163
+ dest="packages",
164
+ default=[])
165
+ parser.add_option('--class',
166
+ help="Select only named classes (wildcards allowed)",
167
+ action="append",
168
+ metavar="NAME",
169
+ dest="class_matchers",
170
+ default=[])
171
+ if interactive:
172
+ parser.add_option('-i', '--interactive',
173
+ help="Ask before doing anything "
174
+ "(use twice to be more careful)",
175
+ action="count",
176
+ dest="interactive",
177
+ default=0)
178
+ parser.add_option('--egg',
179
+ help="Select modules from the given Egg, "
180
+ "using sqlobject.txt",
181
+ action="append",
182
+ metavar="EGG_SPEC",
183
+ dest="eggs",
184
+ default=[])
185
+ return parser
186
+
187
+
188
+ class Command(with_metaclass(DeclarativeMeta, object)):
189
+
190
+ min_args = 0
191
+ min_args_error = 'You must provide at least %(min_args)s arguments'
192
+ max_args = 0
193
+ max_args_error = 'You must provide no more than %(max_args)s arguments'
194
+ aliases = ()
195
+ required_args = []
196
+ description = None
197
+
198
+ help = ''
199
+
200
+ def orderClassesByDependencyLevel(self, classes):
201
+ """
202
+ Return classes ordered by their depth in the class dependency
203
+ tree (this is *not* the inheritance tree), from the
204
+ top level (independant) classes to the deepest level.
205
+ The dependency tree is defined by the foreign key relations.
206
+ """
207
+ # @@: written as a self-contained function for now, to prevent
208
+ # having to modify any core SQLObject component and namespace
209
+ # contamination.
210
+ # yemartin - 2006-08-08
211
+
212
+ class SQLObjectCircularReferenceError(Exception):
213
+ pass
214
+
215
+ def findReverseDependencies(cls):
216
+ """
217
+ Return a list of classes that cls depends on. Note that
218
+ "depends on" here mean "has a foreign key pointing to".
219
+ """
220
+ depended = []
221
+ for _col in cls.sqlmeta.columnList:
222
+ if _col.foreignKey:
223
+ other = findClass(_col.foreignKey,
224
+ _col.soClass.sqlmeta.registry)
225
+ if (other is not cls) and (other not in depended):
226
+ depended.append(other)
227
+ return depended
228
+
229
+ # Cache to save already calculated dependency levels.
230
+ dependency_levels = {}
231
+
232
+ def calculateDependencyLevel(cls, dependency_stack=[]):
233
+ """
234
+ Recursively calculate the dependency level of cls, while
235
+ using the dependency_stack to detect any circular reference.
236
+ """
237
+ # Return value from the cache if already calculated
238
+ if cls in dependency_levels:
239
+ return dependency_levels[cls]
240
+ # Check for circular references
241
+ if cls in dependency_stack:
242
+ dependency_stack.append(cls)
243
+ raise SQLObjectCircularReferenceError(
244
+ "Found a circular reference: %s " %
245
+ (' --> '.join([x.__name__ for x in dependency_stack])))
246
+ dependency_stack.append(cls)
247
+ # Recursively inspect dependent classes.
248
+ depended = findReverseDependencies(cls)
249
+ if depended:
250
+ level = max([calculateDependencyLevel(x, dependency_stack)
251
+ for x in depended]) + 1
252
+ else:
253
+ level = 0
254
+ dependency_levels[cls] = level
255
+ return level
256
+
257
+ # Now simply calculate and sort by dependency levels:
258
+ try:
259
+ sorter = []
260
+ for cls in classes:
261
+ level = calculateDependencyLevel(cls)
262
+ sorter.append((level, cls))
263
+ sorter.sort(key=lambda x: x[0])
264
+ ordered_classes = [cls for _, cls in sorter]
265
+ except SQLObjectCircularReferenceError as msg:
266
+ # Failsafe: return the classes as-is if a circular reference
267
+ # prevented the dependency levels to be calculated.
268
+ print("Warning: a circular reference was detected in the "
269
+ "model. Unable to sort the classes by dependency: they "
270
+ "will be treated in alphabetic order. This may or may "
271
+ "not work depending on your database backend. "
272
+ "The error was:\n%s" % msg)
273
+ return classes
274
+ return ordered_classes
275
+
276
+ def __classinit__(cls, new_args):
277
+ if cls.__bases__ == (object,):
278
+ # This abstract base class
279
+ return
280
+ register(cls)
281
+
282
+ def __init__(self, invoked_as, command_name, args, runner):
283
+ self.invoked_as = invoked_as
284
+ self.command_name = command_name
285
+ self.raw_args = args
286
+ self.runner = runner
287
+
288
+ def run(self):
289
+ self.parser.usage = "%%prog [options]\n%s" % self.summary
290
+ if self.help:
291
+ help = textwrap.fill(
292
+ self.help, int(os.environ.get('COLUMNS', 80)) - 4)
293
+ self.parser.usage += '\n' + help
294
+ self.parser.prog = '%s %s' % (
295
+ os.path.basename(self.invoked_as),
296
+ self.command_name)
297
+ if self.description:
298
+ self.parser.description = self.description
299
+ self.options, self.args = self.parser.parse_args(self.raw_args)
300
+ if (getattr(self.options, 'simulate', False)
301
+ and not self.options.verbose):
302
+ self.options.verbose = 1
303
+ if self.min_args is not None and len(self.args) < self.min_args:
304
+ self.runner.invalid(
305
+ self.min_args_error % {'min_args': self.min_args,
306
+ 'actual_args': len(self.args)})
307
+ if self.max_args is not None and len(self.args) > self.max_args:
308
+ self.runner.invalid(
309
+ self.max_args_error % {'max_args': self.max_args,
310
+ 'actual_args': len(self.args)})
311
+ for var_name, option_name in self.required_args:
312
+ if not getattr(self.options, var_name, None):
313
+ self.runner.invalid(
314
+ 'You must provide the option %s' % option_name)
315
+ conf = self.config()
316
+ if conf and conf.get('sys_path'):
317
+ update_sys_path(conf['sys_path'], self.options.verbose)
318
+ if conf and conf.get('database'):
319
+ conn = sqlobject.connectionForURI(conf['database'])
320
+ sqlobject.sqlhub.processConnection = conn
321
+ for egg_spec in getattr(self.options, 'eggs', []):
322
+ self.load_options_from_egg(egg_spec)
323
+ self.command()
324
+
325
+ def classes(self, require_connection=True,
326
+ require_some=False):
327
+ all = []
328
+ for module_name in self.options.modules:
329
+ all.extend(self.classes_from_module(
330
+ moduleloader.load_module(module_name)))
331
+ for package_name in self.options.packages:
332
+ all.extend(self.classes_from_package(package_name))
333
+ for egg_spec in self.options.eggs:
334
+ all.extend(self.classes_from_egg(egg_spec))
335
+ if self.options.class_matchers:
336
+ filtered = []
337
+ for soClass in all:
338
+ name = soClass.__name__
339
+ for matcher in self.options.class_matchers:
340
+ if fnmatch.fnmatch(name, matcher):
341
+ filtered.append(soClass)
342
+ break
343
+ all = filtered
344
+ conn = self.connection()
345
+ if conn:
346
+ for soClass in all:
347
+ soClass._connection = conn
348
+ else:
349
+ missing = []
350
+ for soClass in all:
351
+ try:
352
+ if not soClass._connection:
353
+ missing.append(soClass)
354
+ except AttributeError:
355
+ missing.append(soClass)
356
+ if missing and require_connection:
357
+ self.runner.invalid(
358
+ 'These classes do not have connections set:\n * %s\n'
359
+ 'You must indicate --connection=URI'
360
+ % '\n * '.join([soClass.__name__
361
+ for soClass in missing]))
362
+ if require_some and not all:
363
+ print('No classes found!')
364
+ if self.options.modules:
365
+ print('Looked in modules: %s' %
366
+ ', '.join(self.options.modules))
367
+ else:
368
+ print('No modules specified')
369
+ if self.options.packages:
370
+ print('Looked in packages: %s' %
371
+ ', '.join(self.options.packages))
372
+ else:
373
+ print('No packages specified')
374
+ if self.options.class_matchers:
375
+ print('Matching class pattern: %s' %
376
+ self.options.class_matches)
377
+ if self.options.eggs:
378
+ print('Looked in eggs: %s' % ', '.join(self.options.eggs))
379
+ else:
380
+ print('No eggs specified')
381
+ sys.exit(1)
382
+ return self.orderClassesByDependencyLevel(all)
383
+
384
+ def classes_from_module(self, module):
385
+ all = []
386
+ if hasattr(module, 'soClasses'):
387
+ for name_or_class in module.soClasses:
388
+ if isinstance(name_or_class, str):
389
+ name_or_class = getattr(module, name_or_class)
390
+ all.append(name_or_class)
391
+ else:
392
+ for name in dir(module):
393
+ value = getattr(module, name)
394
+ if (isinstance(value, type)
395
+ and issubclass(value, sqlobject.SQLObject)
396
+ and value.__module__ == module.__name__):
397
+ all.append(value)
398
+ return all
399
+
400
+ def connection(self):
401
+ config = self.config()
402
+ if config is not None:
403
+ assert config.get('database'), (
404
+ "No database variable found in config file %s"
405
+ % self.options.config_file)
406
+ return sqlobject.connectionForURI(config['database'])
407
+ elif getattr(self.options, 'connection_uri', None):
408
+ return sqlobject.connectionForURI(self.options.connection_uri)
409
+ else:
410
+ return None
411
+
412
+ def config(self):
413
+ if not getattr(self.options, 'config_file', None):
414
+ return None
415
+ config_file = self.options.config_file
416
+ if appconfig:
417
+ if (not config_file.startswith('egg:')
418
+ and not config_file.startswith('config:')):
419
+ config_file = 'config:' + config_file
420
+ return appconfig(config_file,
421
+ relative_to=os.getcwd())
422
+ else:
423
+ return self.ini_config(config_file)
424
+
425
+ def ini_config(self, conf_fn):
426
+ conf_section = 'main'
427
+ if '#' in conf_fn:
428
+ conf_fn, conf_section = conf_fn.split('#', 1)
429
+
430
+ try:
431
+ from ConfigParser import ConfigParser
432
+ except ImportError:
433
+ from configparser import ConfigParser
434
+ p = ConfigParser()
435
+ # Case-sensitive:
436
+ p.optionxform = str
437
+ if not os.path.exists(conf_fn):
438
+ # Stupid RawConfigParser doesn't give an error for
439
+ # non-existant files:
440
+ raise OSError(
441
+ "Config file %s does not exist" % self.options.config_file)
442
+ p.read([conf_fn])
443
+ p._defaults.setdefault(
444
+ 'here', os.path.dirname(os.path.abspath(conf_fn)))
445
+
446
+ possible_sections = []
447
+ for section in p.sections():
448
+ name = section.strip().lower()
449
+ if (conf_section == name
450
+ or (conf_section == name.split(':')[-1]
451
+ and name.split(':')[0] in ('app', 'application'))):
452
+ possible_sections.append(section)
453
+
454
+ if not possible_sections:
455
+ raise OSError(
456
+ "Config file %s does not have a section [%s] or [*:%s]"
457
+ % (conf_fn, conf_section, conf_section))
458
+ if len(possible_sections) > 1:
459
+ raise OSError(
460
+ "Config file %s has multiple sections matching %s: %s"
461
+ % (conf_fn, conf_section, ', '.join(possible_sections)))
462
+
463
+ config = {}
464
+ for op in p.options(possible_sections[0]):
465
+ config[op] = p.get(possible_sections[0], op)
466
+ return config
467
+
468
+ def classes_from_package(self, package_name):
469
+ all = []
470
+ package = moduleloader.load_module(package_name)
471
+ package_dir = os.path.dirname(package.__file__)
472
+
473
+ def find_classes_in_file(arg, dir_name, filenames):
474
+ if dir_name.startswith('.svn'):
475
+ return
476
+ filenames = filter(
477
+ lambda fname: fname.endswith('.py') and fname != '__init__.py',
478
+ filenames)
479
+ for fname in filenames:
480
+ module_name = os.path.join(dir_name, fname)
481
+ module_name = module_name[module_name.find(package_name):]
482
+ module_name = module_name.replace(os.path.sep, '.')[:-3]
483
+ try:
484
+ module = moduleloader.load_module(module_name)
485
+ except ImportError as err:
486
+ if self.options.verbose:
487
+ print('Could not import module "%s". '
488
+ 'Error was : "%s"' % (module_name, err))
489
+ continue
490
+ except Exception as exc:
491
+ if self.options.verbose:
492
+ print('Unknown exception while processing module '
493
+ '"%s" : "%s"' % (module_name, exc))
494
+ continue
495
+ classes = self.classes_from_module(module)
496
+ all.extend(classes)
497
+
498
+ for dirpath, dirnames, filenames in os.walk(package_dir):
499
+ find_classes_in_file(None, dirpath, dirnames + filenames)
500
+ return all
501
+
502
+ def classes_from_egg(self, egg_spec):
503
+ modules = []
504
+ dist, conf = self.config_from_egg(egg_spec, warn_no_sqlobject=True)
505
+ for mod in conf.get('db_module', '').split(','):
506
+ mod = mod.strip()
507
+ if not mod:
508
+ continue
509
+ if self.options.verbose:
510
+ print('Looking in module %s' % mod)
511
+ modules.extend(self.classes_from_module(
512
+ moduleloader.load_module(mod)))
513
+ return modules
514
+
515
+ def load_options_from_egg(self, egg_spec):
516
+ dist, conf = self.config_from_egg(egg_spec)
517
+ if (hasattr(self.options, 'output_dir')
518
+ and not self.options.output_dir and conf.get('history_dir')):
519
+ dir = conf['history_dir']
520
+ dir = dir.replace('$base', dist.location)
521
+ self.options.output_dir = dir
522
+
523
+ def config_from_egg(self, egg_spec, warn_no_sqlobject=True):
524
+ import pkg_resources
525
+ dist = pkg_resources.get_distribution(egg_spec)
526
+ if not dist.has_metadata('sqlobject.txt'):
527
+ if warn_no_sqlobject:
528
+ print('No sqlobject.txt in %s egg info' % egg_spec)
529
+ return None, {}
530
+ result = {}
531
+ for line in dist.get_metadata_lines('sqlobject.txt'):
532
+ line = line.strip()
533
+ if not line or line.startswith('#'):
534
+ continue
535
+ name, value = line.split('=', 1)
536
+ name = name.strip().lower()
537
+ if name in result:
538
+ print('Warning: %s appears more than once '
539
+ 'in sqlobject.txt' % name)
540
+ result[name.strip().lower()] = value.strip()
541
+ return dist, result
542
+
543
+ def command(self):
544
+ raise NotImplementedError
545
+
546
+ def _get_prog_name(self):
547
+ return os.path.basename(self.invoked_as)
548
+ prog_name = property(_get_prog_name)
549
+
550
+ def ask(self, prompt, safe=False, default=True):
551
+ if self.options.interactive >= 2:
552
+ default = safe
553
+ if default:
554
+ prompt += ' [Y/n]? '
555
+ else:
556
+ prompt += ' [y/N]? '
557
+ while 1:
558
+ response = input(prompt).strip()
559
+ if not response.strip():
560
+ return default
561
+ if response and response[0].lower() in ('y', 'n'):
562
+ return response[0].lower() == 'y'
563
+ print('Y or N please')
564
+
565
+ def shorten_filename(self, fn):
566
+ """
567
+ Shortens a filename to make it relative to the current
568
+ directory (if it can). For display purposes.
569
+ """
570
+ if fn.startswith(os.getcwd() + '/'):
571
+ fn = fn[len(os.getcwd()) + 1:]
572
+ return fn
573
+
574
+ def open_editor(self, pretext, breaker=None, extension='.txt'):
575
+ """
576
+ Open an editor with the given text. Return the new text,
577
+ or None if no edits were made. If given, everything after
578
+ `breaker` will be ignored.
579
+ """
580
+ fn = nowarning_tempnam() + extension
581
+ f = open(fn, 'w')
582
+ f.write(pretext)
583
+ f.close()
584
+ print('$EDITOR %s' % fn)
585
+ os.system('$EDITOR %s' % fn)
586
+ f = open(fn, 'r')
587
+ content = f.read()
588
+ f.close()
589
+ if breaker:
590
+ content = content.split(breaker)[0]
591
+ pretext = pretext.split(breaker)[0]
592
+ if content == pretext or not content.strip():
593
+ return None
594
+ return content
595
+
596
+
597
+ class CommandSQL(Command):
598
+
599
+ name = 'sql'
600
+ summary = 'Show SQL CREATE statements'
601
+
602
+ parser = standard_parser(simulate=False)
603
+
604
+ def command(self):
605
+ classes = self.classes()
606
+ allConstraints = []
607
+ for cls in classes:
608
+ if self.options.verbose >= 1:
609
+ print('-- %s from %s' % (
610
+ cls.__name__, cls.__module__))
611
+ createSql, constraints = cls.createTableSQL()
612
+ print(createSql.strip() + ';\n')
613
+ allConstraints.append(constraints)
614
+ for constraints in allConstraints:
615
+ if constraints:
616
+ for constraint in constraints:
617
+ if constraint:
618
+ print(constraint.strip() + ';\n')
619
+
620
+
621
+ class CommandList(Command):
622
+
623
+ name = 'list'
624
+ summary = 'Show all SQLObject classes found'
625
+
626
+ parser = standard_parser(simulate=False, connection=False)
627
+
628
+ def command(self):
629
+ if self.options.verbose >= 1:
630
+ print('Classes found:')
631
+ classes = self.classes(require_connection=False)
632
+ for soClass in classes:
633
+ print('%s.%s' % (soClass.__module__, soClass.__name__))
634
+ if self.options.verbose >= 1:
635
+ print(' Table: %s' % soClass.sqlmeta.table)
636
+
637
+
638
+ class CommandCreate(Command):
639
+
640
+ name = 'create'
641
+ summary = 'Create tables'
642
+
643
+ parser = standard_parser(interactive=True)
644
+ parser.add_option('--create-db',
645
+ action='store_true',
646
+ dest='create_db',
647
+ help="Create the database")
648
+
649
+ def command(self):
650
+ v = self.options.verbose
651
+ created = 0
652
+ existing = 0
653
+ dbs_created = []
654
+ constraints = {}
655
+ for soClass in self.classes(require_some=True):
656
+ if (self.options.create_db
657
+ and soClass._connection not in dbs_created):
658
+ if not self.options.simulate:
659
+ try:
660
+ soClass._connection.createEmptyDatabase()
661
+ except soClass._connection.module.ProgrammingError as e:
662
+ if str(e).find('already exists') != -1:
663
+ print('Database already exists')
664
+ else:
665
+ raise
666
+ else:
667
+ print('(simulating; cannot create database)')
668
+ dbs_created.append(soClass._connection)
669
+ if soClass._connection not in constraints.keys():
670
+ constraints[soClass._connection] = []
671
+ exists = soClass._connection.tableExists(soClass.sqlmeta.table)
672
+ if v >= 1:
673
+ if exists:
674
+ existing += 1
675
+ print('%s already exists.' % soClass.__name__)
676
+ else:
677
+ print('Creating %s' % soClass.__name__)
678
+ if v >= 2:
679
+ sql, extra = soClass.createTableSQL()
680
+ print(sql)
681
+ if (not self.options.simulate and not exists):
682
+ if self.options.interactive:
683
+ if self.ask('Create %s' % soClass.__name__):
684
+ created += 1
685
+ tableConstraints = soClass.createTable(
686
+ applyConstraints=False)
687
+ if tableConstraints:
688
+ constraints[soClass._connection].append(
689
+ tableConstraints)
690
+ else:
691
+ print('Cancelled')
692
+ else:
693
+ created += 1
694
+ tableConstraints = soClass.createTable(
695
+ applyConstraints=False)
696
+ if tableConstraints:
697
+ constraints[soClass._connection].append(
698
+ tableConstraints)
699
+ for connection in constraints.keys():
700
+ if v >= 2:
701
+ print('Creating constraints')
702
+ for constraintList in constraints[connection]:
703
+ for constraint in constraintList:
704
+ if constraint:
705
+ connection.query(constraint)
706
+ if v >= 1:
707
+ print('%i tables created (%i already exist)' % (
708
+ created, existing))
709
+
710
+
711
+ class CommandDrop(Command):
712
+
713
+ name = 'drop'
714
+ summary = 'Drop tables'
715
+
716
+ parser = standard_parser(interactive=True)
717
+
718
+ def command(self):
719
+ v = self.options.verbose
720
+ dropped = 0
721
+ not_existing = 0
722
+ for soClass in reversed(self.classes()):
723
+ exists = soClass._connection.tableExists(soClass.sqlmeta.table)
724
+ if v >= 1:
725
+ if exists:
726
+ print('Dropping %s' % soClass.__name__)
727
+ else:
728
+ not_existing += 1
729
+ print('%s does not exist.' % soClass.__name__)
730
+ if (not self.options.simulate and exists):
731
+ if self.options.interactive:
732
+ if self.ask('Drop %s' % soClass.__name__):
733
+ dropped += 1
734
+ soClass.dropTable()
735
+ else:
736
+ print('Cancelled')
737
+ else:
738
+ dropped += 1
739
+ soClass.dropTable()
740
+ if v >= 1:
741
+ print('%i tables dropped (%i didn\'t exist)' % (
742
+ dropped, not_existing))
743
+
744
+
745
+ class CommandStatus(Command):
746
+
747
+ name = 'status'
748
+ summary = 'Show status of classes vs. database'
749
+ help = ('This command checks the SQLObject definition and checks if '
750
+ 'the tables in the database match. It can always test for '
751
+ 'missing tables, and on some databases can test for the '
752
+ 'existance of other tables. Column types are not currently '
753
+ 'checked.')
754
+
755
+ parser = standard_parser(simulate=False)
756
+
757
+ def print_class(self, soClass):
758
+ if self.printed:
759
+ return
760
+ self.printed = True
761
+ print('Checking %s...' % soClass.__name__)
762
+
763
+ def command(self):
764
+ good = 0
765
+ bad = 0
766
+ missing_tables = 0
767
+ columnsFromSchema_warning = False
768
+ for soClass in self.classes(require_some=True):
769
+ conn = soClass._connection
770
+ self.printed = False
771
+ if self.options.verbose:
772
+ self.print_class(soClass)
773
+ if not conn.tableExists(soClass.sqlmeta.table):
774
+ self.print_class(soClass)
775
+ print(' Does not exist in database')
776
+ missing_tables += 1
777
+ continue
778
+ try:
779
+ columns = conn.columnsFromSchema(soClass.sqlmeta.table,
780
+ soClass)
781
+ except AttributeError:
782
+ if not columnsFromSchema_warning:
783
+ print('Database does not support reading columns')
784
+ columnsFromSchema_warning = True
785
+ good += 1
786
+ continue
787
+ except AssertionError as e:
788
+ print('Cannot read db table %s: %s' % (
789
+ soClass.sqlmeta.table, e))
790
+ continue
791
+ existing = {}
792
+ for _col in columns:
793
+ _col = _col.withClass(soClass)
794
+ existing[_col.dbName] = _col
795
+ missing = {}
796
+ for _col in soClass.sqlmeta.columnList:
797
+ if _col.dbName in existing:
798
+ del existing[_col.dbName]
799
+ else:
800
+ missing[_col.dbName] = _col
801
+ if existing:
802
+ self.print_class(soClass)
803
+ for _col in existing.values():
804
+ print(' Database has extra column: %s' % _col.dbName)
805
+ if missing:
806
+ self.print_class(soClass)
807
+ for _col in missing.values():
808
+ print(' Database missing column: %s' % _col.dbName)
809
+ if existing or missing:
810
+ bad += 1
811
+ else:
812
+ good += 1
813
+ if self.options.verbose:
814
+ print('%i in sync; %i out of sync; %i not in database' % (
815
+ good, bad, missing_tables))
816
+
817
+
818
+ class CommandHelp(Command):
819
+
820
+ name = 'help'
821
+ summary = 'Show help'
822
+
823
+ parser = optparse.OptionParser()
824
+
825
+ max_args = 1
826
+
827
+ def command(self):
828
+ if self.args:
829
+ the_runner.run([self.invoked_as, self.args[0], '-h'])
830
+ else:
831
+ print('Available commands:')
832
+ print(' (use "%s help COMMAND" or "%s COMMAND -h" ' % (
833
+ self.prog_name, self.prog_name))
834
+ print(' for more information)')
835
+ items = sorted(the_runner.commands.items())
836
+ max_len = max([len(cn) for cn, c in items])
837
+ for command_name, command in items:
838
+ print('%s:%s %s' % (command_name,
839
+ ' ' * (max_len - len(command_name)),
840
+ command.summary))
841
+ if command.aliases:
842
+ print('%s (Aliases: %s)' % (
843
+ ' ' * max_len, ', '.join(command.aliases)))
844
+
845
+
846
+ class CommandExecute(Command):
847
+
848
+ name = 'execute'
849
+ summary = 'Execute SQL statements'
850
+ help = ('Runs SQL statements directly in the database, with no '
851
+ 'intervention. Useful when used with a configuration file. '
852
+ 'Each argument is executed as an individual statement.')
853
+
854
+ parser = standard_parser(find_modules=False)
855
+ parser.add_option('--stdin',
856
+ help="Read SQL from stdin "
857
+ "(normally takes SQL from the command line)",
858
+ dest="use_stdin",
859
+ action="store_true")
860
+
861
+ max_args = None
862
+
863
+ def command(self):
864
+ args = self.args
865
+ if self.options.use_stdin:
866
+ if self.options.verbose:
867
+ print("Reading additional SQL from stdin "
868
+ "(Ctrl-D or Ctrl-Z to finish)...")
869
+ args.append(sys.stdin.read())
870
+ self.conn = self.connection().getConnection()
871
+ self.cursor = self.conn.cursor()
872
+ for sql in args:
873
+ self.execute_sql(sql)
874
+
875
+ def execute_sql(self, sql):
876
+ if self.options.verbose:
877
+ print(sql)
878
+ try:
879
+ self.cursor.execute(sql)
880
+ except Exception as e:
881
+ if not self.options.verbose:
882
+ print(sql)
883
+ print("****Error:")
884
+ print(' ', e)
885
+ return
886
+ desc = self.cursor.description
887
+ rows = self.cursor.fetchall()
888
+ if self.options.verbose:
889
+ if not self.cursor.rowcount:
890
+ print("No rows accessed")
891
+ else:
892
+ print("%i rows accessed" % self.cursor.rowcount)
893
+ if desc:
894
+ for (name, type_code, display_size, internal_size,
895
+ precision, scale, null_ok) in desc:
896
+ sys.stdout.write("%s\t" % name)
897
+ sys.stdout.write("\n")
898
+ for row in rows:
899
+ for _col in row:
900
+ sys.stdout.write("%r\t" % _col)
901
+ sys.stdout.write("\n")
902
+ print()
903
+
904
+
905
+ class CommandRecord(Command):
906
+
907
+ name = 'record'
908
+ summary = 'Record historical information about the database status'
909
+ help = ('Record state of table definitions. The state of each '
910
+ 'table is written out to a separate file in a directory, '
911
+ 'and that directory forms a "version". A table is also '
912
+ 'added to your database (%s) that reflects the version the '
913
+ 'database is currently at. Use the upgrade command to '
914
+ 'sync databases with code.'
915
+ % SQLObjectVersionTable.sqlmeta.table)
916
+
917
+ parser = standard_parser()
918
+ parser.add_option('--output-dir',
919
+ help="Base directory for recorded definitions",
920
+ dest="output_dir",
921
+ metavar="DIR",
922
+ default=None)
923
+ parser.add_option('--no-db-record',
924
+ help="Don't record version to database",
925
+ dest="db_record",
926
+ action="store_false",
927
+ default=True)
928
+ parser.add_option('--force-create',
929
+ help="Create a new version even if appears to be "
930
+ "identical to the last version",
931
+ action="store_true",
932
+ dest="force_create")
933
+ parser.add_option('--name',
934
+ help="The name to append to the version. The "
935
+ "version should sort after previous versions (so "
936
+ "any versions from the same day should come "
937
+ "alphabetically before this version).",
938
+ dest="version_name",
939
+ metavar="NAME")
940
+ parser.add_option('--force-db-version',
941
+ help="Update the database version, and include no "
942
+ "database information. This is for databases that "
943
+ "were developed without any interaction with "
944
+ "this tool, to create a 'beginning' revision.",
945
+ metavar="VERSION_NAME",
946
+ dest="force_db_version")
947
+ parser.add_option('--edit',
948
+ help="Open an editor for the upgrader in the last "
949
+ "version (using $EDITOR).",
950
+ action="store_true",
951
+ dest="open_editor")
952
+
953
+ version_regex = re.compile(r'^\d\d\d\d-\d\d-\d\d')
954
+
955
+ def command(self):
956
+ if self.options.force_db_version:
957
+ self.command_force_db_version()
958
+ return
959
+
960
+ v = self.options.verbose
961
+ sim = self.options.simulate
962
+ classes = self.classes()
963
+ if not classes:
964
+ print("No classes found!")
965
+ return
966
+
967
+ output_dir = self.find_output_dir()
968
+ version = os.path.basename(output_dir)
969
+ print("Creating version %s" % version)
970
+ conns = []
971
+ files = {}
972
+ for cls in self.classes():
973
+ dbName = cls._connection.dbName
974
+ if cls._connection not in conns:
975
+ conns.append(cls._connection)
976
+ fn = os.path.join(cls.__name__ + '_' + dbName + '.sql')
977
+ if sim:
978
+ continue
979
+ create, constraints = cls.createTableSQL()
980
+ if constraints:
981
+ constraints = '\n-- Constraints:\n%s\n' % (
982
+ '\n'.join(constraints))
983
+ else:
984
+ constraints = ''
985
+ files[fn] = ''.join([
986
+ '-- Exported definition from %s\n'
987
+ % time.strftime('%Y-%m-%dT%H:%M:%S'),
988
+ '-- Class %s.%s\n'
989
+ % (cls.__module__, cls.__name__),
990
+ '-- Database: %s\n'
991
+ % dbName,
992
+ create.strip(),
993
+ '\n',
994
+ constraints])
995
+ last_version_dir = self.find_last_version()
996
+ if last_version_dir and not self.options.force_create:
997
+ if v > 1:
998
+ print("Checking %s to see if it is current" % last_version_dir)
999
+ files_copy = files.copy()
1000
+ for fn in os.listdir(last_version_dir):
1001
+ if not fn.endswith('.sql'):
1002
+ continue
1003
+ if fn not in files_copy:
1004
+ if v > 1:
1005
+ print("Missing file %s" % fn)
1006
+ break
1007
+ f = open(os.path.join(last_version_dir, fn), 'r')
1008
+ content = f.read()
1009
+ f.close()
1010
+ if (self.strip_comments(files_copy[fn])
1011
+ != self.strip_comments(content)):
1012
+ if v > 1:
1013
+ print("Content does not match: %s" % fn)
1014
+ break
1015
+ del files_copy[fn]
1016
+ else:
1017
+ # No differences so far
1018
+ if not files_copy:
1019
+ # Used up all files
1020
+ print("Current status matches version %s"
1021
+ % os.path.basename(last_version_dir))
1022
+ return
1023
+ if v > 1:
1024
+ print("Extra files: %s" % ', '.join(files_copy.keys()))
1025
+ if v:
1026
+ print("Current state does not match %s"
1027
+ % os.path.basename(last_version_dir))
1028
+ if v > 1 and not last_version_dir:
1029
+ print("No last version to check")
1030
+ if not sim:
1031
+ os.mkdir(output_dir)
1032
+ if v:
1033
+ print('Making directory %s' % self.shorten_filename(output_dir))
1034
+ files = sorted(files.items())
1035
+ for fn, content in files:
1036
+ if v:
1037
+ print(' Writing %s' % self.shorten_filename(fn))
1038
+ if not sim:
1039
+ f = open(os.path.join(output_dir, fn), 'w')
1040
+ f.write(content)
1041
+ f.close()
1042
+ if self.options.db_record:
1043
+ all_diffs = []
1044
+ for cls in self.classes():
1045
+ for conn in conns:
1046
+ diffs = db_differences(cls, conn)
1047
+ for diff in diffs:
1048
+ if len(conns) > 1:
1049
+ diff = ' (%s).%s: %s' % (
1050
+ conn.uri(), cls.sqlmeta.table, diff)
1051
+ else:
1052
+ diff = ' %s: %s' % (cls.sqlmeta.table, diff)
1053
+ all_diffs.append(diff)
1054
+ if all_diffs:
1055
+ print('Database does not match schema:')
1056
+ print('\n'.join(all_diffs))
1057
+ for conn in conns:
1058
+ self.update_db(version, conn)
1059
+ else:
1060
+ all_diffs = []
1061
+ if self.options.open_editor:
1062
+ if not last_version_dir:
1063
+ print("Cannot edit upgrader because there is no "
1064
+ "previous version")
1065
+ else:
1066
+ breaker = ('-' * 20 + ' lines below this will be ignored '
1067
+ + '-' * 20)
1068
+ pre_text = breaker + '\n' + '\n'.join(all_diffs)
1069
+ text = self.open_editor('\n\n' + pre_text, breaker=breaker,
1070
+ extension='.sql')
1071
+ if text is not None:
1072
+ fn = os.path.join(last_version_dir,
1073
+ 'upgrade_%s_%s.sql' %
1074
+ (dbName, version))
1075
+ f = open(fn, 'w')
1076
+ f.write(text)
1077
+ f.close()
1078
+ print('Wrote to %s' % fn)
1079
+
1080
+ def update_db(self, version, conn):
1081
+ v = self.options.verbose
1082
+ if not conn.tableExists(SQLObjectVersionTable.sqlmeta.table):
1083
+ if v:
1084
+ print('Creating table %s'
1085
+ % SQLObjectVersionTable.sqlmeta.table)
1086
+ sql = SQLObjectVersionTable.createTableSQL(connection=conn)
1087
+ if v > 1:
1088
+ print(sql)
1089
+ if not self.options.simulate:
1090
+ SQLObjectVersionTable.createTable(connection=conn)
1091
+ if not self.options.simulate:
1092
+ SQLObjectVersionTable.clearTable(connection=conn)
1093
+ SQLObjectVersionTable(
1094
+ version=version,
1095
+ connection=conn)
1096
+
1097
+ def strip_comments(self, sql):
1098
+ lines = [_l for _l in sql.splitlines()
1099
+ if not _l.strip().startswith('--')]
1100
+ return '\n'.join(lines)
1101
+
1102
+ def base_dir(self):
1103
+ base = self.options.output_dir
1104
+ if base is None:
1105
+ config = self.config()
1106
+ if config is not None:
1107
+ base = config.get('sqlobject_history_dir', '.')
1108
+ else:
1109
+ base = '.'
1110
+ if not os.path.exists(base):
1111
+ print('Creating history directory %s' %
1112
+ self.shorten_filename(base))
1113
+ if not self.options.simulate:
1114
+ os.makedirs(base)
1115
+ return base
1116
+
1117
+ def find_output_dir(self):
1118
+ today = time.strftime('%Y-%m-%d', time.localtime())
1119
+ if self.options.version_name:
1120
+ dir = os.path.join(self.base_dir(), today + '-'
1121
+ + self.options.version_name)
1122
+ if os.path.exists(dir):
1123
+ print("Error, directory already exists: %s"
1124
+ % dir)
1125
+ sys.exit(1)
1126
+ return dir
1127
+ extra = ''
1128
+ while 1:
1129
+ dir = os.path.join(self.base_dir(), today + extra)
1130
+ if not os.path.exists(dir):
1131
+ return dir
1132
+ if not extra:
1133
+ extra = 'a'
1134
+ else:
1135
+ extra = chr(ord(extra) + 1)
1136
+
1137
+ def find_last_version(self):
1138
+ names = []
1139
+ for fn in os.listdir(self.base_dir()):
1140
+ if not self.version_regex.search(fn):
1141
+ continue
1142
+ names.append(fn)
1143
+ if not names:
1144
+ return None
1145
+ names.sort()
1146
+ return os.path.join(self.base_dir(), names[-1])
1147
+
1148
+ def command_force_db_version(self):
1149
+ v = self.options.verbose
1150
+ sim = self.options.simulate
1151
+ version = self.options.force_db_version
1152
+ if not self.version_regex.search(version):
1153
+ print("Versions must be in the format YYYY-MM-DD...")
1154
+ print("You version %s does not fit this" % version)
1155
+ return
1156
+ version_dir = os.path.join(self.base_dir(), version)
1157
+ if not os.path.exists(version_dir):
1158
+ if v:
1159
+ print('Creating %s' % self.shorten_filename(version_dir))
1160
+ if not sim:
1161
+ os.mkdir(version_dir)
1162
+ elif v:
1163
+ print('Directory %s exists'
1164
+ % self.shorten_filename(version_dir))
1165
+ if self.options.db_record:
1166
+ self.update_db(version, self.connection())
1167
+
1168
+
1169
+ class CommandUpgrade(CommandRecord):
1170
+
1171
+ name = 'upgrade'
1172
+ summary = 'Update the database to a new version (as created by record)'
1173
+ help = ('This command runs scripts (that you write by hand) to '
1174
+ 'upgrade a database. The database\'s current version is in '
1175
+ 'the sqlobject_version table (use record --force-db-version '
1176
+ 'if a database does not have a sqlobject_version table), '
1177
+ 'and upgrade scripts are in the version directory you are '
1178
+ 'upgrading FROM, named upgrade_DBNAME_VERSION.sql, like '
1179
+ '"upgrade_mysql_2004-12-01b.sql".')
1180
+
1181
+ parser = standard_parser(find_modules=False)
1182
+ parser.add_option('--upgrade-to',
1183
+ help="Upgrade to the given version "
1184
+ "(default: newest version)",
1185
+ dest="upgrade_to",
1186
+ metavar="VERSION")
1187
+ parser.add_option('--output-dir',
1188
+ help="Base directory for recorded definitions",
1189
+ dest="output_dir",
1190
+ metavar="DIR",
1191
+ default=None)
1192
+
1193
+ upgrade_regex = re.compile(r'^upgrade_([a-z]*)_([^.]*)\.sql$', re.I)
1194
+
1195
+ def command(self):
1196
+ v = self.options.verbose
1197
+ sim = self.options.simulate
1198
+ if self.options.upgrade_to:
1199
+ version_to = self.options.upgrade_to
1200
+ else:
1201
+ fname = self.find_last_version()
1202
+ if fname is None:
1203
+ print("No version exists, use 'record' command to create one")
1204
+ return
1205
+ version_to = os.path.basename(fname)
1206
+ current = self.current_version()
1207
+ if v:
1208
+ print('Current version: %s' % current)
1209
+ version_list = self.make_plan(current, version_to)
1210
+ if not version_list:
1211
+ print('Database up to date')
1212
+ return
1213
+ if v:
1214
+ print('Plan:')
1215
+ for next_version, upgrader in version_list:
1216
+ print(' Use %s to upgrade to %s' % (
1217
+ self.shorten_filename(upgrader), next_version))
1218
+ conn = self.connection()
1219
+ for next_version, upgrader in version_list:
1220
+ f = open(upgrader)
1221
+ sql = f.read()
1222
+ f.close()
1223
+ if v:
1224
+ print("Running:")
1225
+ print(sql)
1226
+ print('-' * 60)
1227
+ if not sim:
1228
+ try:
1229
+ conn.query(sql)
1230
+ except Exception:
1231
+ print("Error in script: %s" % upgrader)
1232
+ raise
1233
+ self.update_db(next_version, conn)
1234
+ print('Done.')
1235
+
1236
+ def current_version(self):
1237
+ conn = self.connection()
1238
+ if not conn.tableExists(SQLObjectVersionTable.sqlmeta.table):
1239
+ print('No sqlobject_version table!')
1240
+ sys.exit(1)
1241
+ versions = list(SQLObjectVersionTable.select(connection=conn))
1242
+ if not versions:
1243
+ print('No rows in sqlobject_version!')
1244
+ sys.exit(1)
1245
+ if len(versions) > 1:
1246
+ print('Ambiguous sqlobject_version_table')
1247
+ sys.exit(1)
1248
+ return versions[0].version
1249
+
1250
+ def make_plan(self, current, dest):
1251
+ if current == dest:
1252
+ return []
1253
+ dbname = self.connection().dbName
1254
+ next_version, upgrader = self.best_upgrade(current, dest, dbname)
1255
+ if not upgrader:
1256
+ print('No way to upgrade from %s to %s' % (current, dest))
1257
+ print('(you need a %s/upgrade_%s_%s.sql script)'
1258
+ % (current, dbname, dest))
1259
+ sys.exit(1)
1260
+ plan = [(next_version, upgrader)]
1261
+ if next_version == dest:
1262
+ return plan
1263
+ else:
1264
+ return plan + self.make_plan(next_version, dest)
1265
+
1266
+ def best_upgrade(self, current, dest, target_dbname):
1267
+ current_dir = os.path.join(self.base_dir(), current)
1268
+ if self.options.verbose > 1:
1269
+ print('Looking in %s for upgraders'
1270
+ % self.shorten_filename(current_dir))
1271
+ upgraders = []
1272
+ for fn in os.listdir(current_dir):
1273
+ match = self.upgrade_regex.search(fn)
1274
+ if not match:
1275
+ if self.options.verbose > 1:
1276
+ print('Not an upgrade script: %s' % fn)
1277
+ continue
1278
+ dbname = match.group(1)
1279
+ version = match.group(2)
1280
+ if dbname != target_dbname:
1281
+ if self.options.verbose > 1:
1282
+ print('Not for this database: %s (want %s)' % (
1283
+ dbname, target_dbname))
1284
+ continue
1285
+ if version > dest:
1286
+ if self.options.verbose > 1:
1287
+ print('Version too new: %s (only want %s)' % (
1288
+ version, dest))
1289
+ upgraders.append((version, os.path.join(current_dir, fn)))
1290
+ if not upgraders:
1291
+ if self.options.verbose > 1:
1292
+ print('No upgraders found in %s' % current_dir)
1293
+ return None, None
1294
+ upgraders.sort()
1295
+ return upgraders[-1]
1296
+
1297
+
1298
+ def update_sys_path(paths, verbose):
1299
+ if isinstance(paths, string_type):
1300
+ paths = [paths]
1301
+ for path in paths:
1302
+ path = os.path.abspath(path)
1303
+ if path not in sys.path:
1304
+ if verbose > 1:
1305
+ print('Adding %s to path' % path)
1306
+ sys.path.insert(0, path)
1307
+
1308
+ if __name__ == '__main__':
1309
+ the_runner.run(sys.argv)