generate.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. #!/usr/bin/env python
  2. import re
  3. import MySQLdb as mysql
  4. from singplur import singularize
  5. from itertools import combinations
  6. PRIMARY_KEY = 'id'
  7. TAB = '\t'
  8. MODEL_NAMESPACE = ''
  9. def php_value(data, indent=0):
  10. """
  11. Convert Python data types to PHP code. Tuples or lists are used for
  12. single-line arrays, sets for multiline arrays, dictionaries for associative
  13. arrays, and strings are encapsulated in single quotes.
  14. """
  15. if isinstance(data, str):
  16. return "'%s'" % data.replace("'", "\\'")
  17. tabs = (indent + 1) * TAB
  18. if type(data) in (list, tuple):
  19. return 'array(%s)' % ', '.join(map(php_value, data))
  20. elif isinstance(data, dict):
  21. if not data:
  22. return 'array()'
  23. lines = ["'%s' => %s" % (k, php_value(v, indent + 1))
  24. for k, v in data.iteritems()]
  25. elif isinstance(data, set):
  26. if not data:
  27. return 'array()'
  28. lines = [php_value(item, indent + 1) for item in data]
  29. else:
  30. return str(data)
  31. return 'array(\n%s\n%s)' % (',\n'.join([tabs + line for line in lines]),
  32. indent * TAB)
  33. def php_assoc((arg, kwargs)):
  34. assocs = ["'%s' => %s" % (k, php_value(v)) for k, v in kwargs.iteritems()]
  35. return 'array(%s)' % ', '.join([php_value(arg)] + assocs)
  36. def php_static_var(name, value):
  37. return 'static $%s = %s;' % (name, value)
  38. def php_static_array(name, values):
  39. array = 'array(%s)' if len(values) < 2 \
  40. else 'array(\n{0}{0}%s\n{0})'.format(TAB)
  41. lines = map(php_assoc, values)
  42. array_values = (',\n' + 2 * TAB).join(lines)
  43. return php_static_var(name, array % array_values)
  44. def php_block(php_code):
  45. return '<?php\n\n%s\n\n?>' % php_code
  46. class Model(object):
  47. def __init__(self, table, options):
  48. self.table = table
  49. self.options = options
  50. self.attributes = []
  51. self.primary_key = []
  52. self.foreign_keys = []
  53. self.has_many = []
  54. self.has_one = []
  55. self.belongs_to = []
  56. def __str__(self):
  57. return '<Model "%s" table=%s>' % (self.name(), self.table)
  58. def singular_name(self):
  59. return singularize(self.table)
  60. def classname(self):
  61. return singularize(re.sub('(?:^|_)([a-z])',
  62. lambda m: m.group(1).upper(), self.table))
  63. def read_attributes(self, conn):
  64. fields = read_fields(conn, self.table)
  65. is_protected = lambda field: field['Key'] in ('PRI', 'MUL')
  66. self.protected_fields, accessible = partition(is_protected, fields)
  67. self.accessible_attr = [field['Field'] for field in accessible]
  68. def process_keys(self, models):
  69. for field in self.protected_fields:
  70. name = field['Field']
  71. if field['Key'] == 'PRI':
  72. self.primary_key.append(name)
  73. elif field['Key'] == 'MUL':
  74. self.add_foreign_key(name, models)
  75. # Has-many through
  76. if len(self.belongs_to) > 1:
  77. relations = [find_model(name, models)
  78. for name, options in self.belongs_to]
  79. for a, b in combinations(relations, 2):
  80. a.add_has_relation(b, through=self.table)
  81. b.add_has_relation(a, through=self.table)
  82. def add_has_relation(self, model, **kwargs):
  83. options = self.relopts(model)
  84. options.update(kwargs)
  85. name = model.singular_name()
  86. if [self.singular_name(), name] in self.options.get('has_one', []):
  87. self.has_one.append((name, options))
  88. else:
  89. self.has_many.append((model.table, options))
  90. def relopts(self, model, **kwargs):
  91. relopts = kwargs
  92. if self.options.get('create_select', True):
  93. relopts['select'] = PRIMARY_KEY + ', ' \
  94. + ', '.join(model.accessible_attr)
  95. return relopts
  96. def add_foreign_key(self, name, models):
  97. self.foreign_keys.append(name)
  98. singular = re.sub('_' + PRIMARY_KEY + '$', '', name)
  99. try:
  100. other_model = find_model(singular, models)
  101. except ValueError:
  102. return
  103. self.belongs_to.append((singular, self.relopts(other_model)))
  104. other_model.add_has_relation(self)
  105. def strip_array(self, attr):
  106. array = getattr(self, attr)
  107. if len(array) == 1:
  108. relation, opts = array[0]
  109. if not len(opts):
  110. return php_static_var(attr, php_value(relation))
  111. def php_has_many(self):
  112. stripped = self.strip_array('has_many')
  113. if stripped:
  114. return stripped
  115. if self.has_many:
  116. return php_static_array('has_many', self.has_many)
  117. def php_has_one(self):
  118. stripped = self.strip_array('has_one')
  119. if stripped:
  120. return stripped
  121. if self.has_one:
  122. return php_static_array('has_one', self.has_one)
  123. def php_belongs_to(self):
  124. stripped = self.strip_array('belongs_to')
  125. if stripped:
  126. return stripped
  127. if self.belongs_to:
  128. return php_static_array('belongs_to', self.belongs_to)
  129. def php_has_many_and_belongs_to(self):
  130. if not self.has_many or not self.belongs_to:
  131. return
  132. def php_attr_accessible(self):
  133. # All attributes except primary/foreign keys are accessible
  134. if self.accessible_attr:
  135. return php_static_var('attr_accessible',
  136. php_value(self.accessible_attr))
  137. def generate_php(self, add_php_block=False):
  138. # PHP delimiters
  139. php = '<?php\n\n%s\n\n?>'
  140. # Class definition
  141. classname = self.classname()
  142. if self.options.get('namespace'):
  143. classname = options['namespace'] + '\\' + classname
  144. classdef = 'class %s extends ActiveRecord\\Model {\n%%s\n}' % classname
  145. # Indented lines within class definition
  146. lines = []
  147. if self.options.get('create_relations', True):
  148. lines += [
  149. self.php_has_many(),
  150. self.php_has_one(),
  151. self.php_belongs_to(),
  152. self.php_has_many_and_belongs_to(),
  153. ]
  154. if self.options.get('create_accessible', True):
  155. lines.append(self.php_attr_accessible())
  156. php = classdef % '\n'.join([TAB + line
  157. for line in lines if line is not None])
  158. return php_block(php) if add_php_block else php
  159. def filename(self):
  160. return self.classname().replace('_', '') + '.php'
  161. def create_path_from_dir(self, dirname):
  162. return os.path.dirname(dirname + '//') + '/' + self.filename()
  163. def save_in_file(self, path):
  164. code = self.generate_php(True)
  165. f = open(path, 'w')
  166. f.write(code)
  167. f.close()
  168. def find_model(singular_name, models):
  169. for model in models:
  170. if model.singular_name() == singular_name:
  171. return model
  172. raise ValueError('No model found for "%s".' % singular_name)
  173. def flatten(iterable):
  174. return reduce(lambda a, b: a + b, map(list, iterable), [])
  175. def create_models(conn, options):
  176. models = [Model(table, options) for table in read_tables(conn)]
  177. for model in models:
  178. model.read_attributes(conn)
  179. for model in models:
  180. model.process_keys(models)
  181. return models
  182. def refine_model(conn, model, models):
  183. for field in read_fields(conn, model.table):
  184. model.add_attribute(field, models)
  185. def read_tables(conn):
  186. cur = conn.cursor()
  187. cur.execute('show tables')
  188. tables = flatten(cur.fetchall())
  189. cur.close()
  190. return tables
  191. def read_fields(conn, table):
  192. cur = conn.cursor(mysql.cursors.DictCursor)
  193. cur.execute('show columns from %s' % table)
  194. fields = cur.fetchall()
  195. cur.close()
  196. return fields
  197. def partition(callback, iterable):
  198. """
  199. Partition an iterable into two parts using a callback that returns a
  200. boolean.
  201. Example:
  202. >>> partition(lambda x: x & 1, range(6))
  203. ([1, 3, 5], [0, 2, 4])
  204. """
  205. a, b = [], []
  206. for item in iterable:
  207. (a if callback(item) else b).append(item)
  208. return a, b
  209. if __name__ == '__main__': # pragma: nocover
  210. import os
  211. from argparse import ArgumentParser
  212. parser = ArgumentParser(description='Generate PHPActiveRecord models.')
  213. parser.add_argument('dbname', help='database name')
  214. parser.add_argument('-H', '--host', metavar='ADDRESS', default='localhost',
  215. help='MySQL server address')
  216. parser.add_argument('-u', '--user', metavar='USERNAME', default='root',
  217. help='MySQL username')
  218. parser.add_argument('-p', '--password', default='', help='MySQL password')
  219. parser.add_argument('--has-one', nargs=2, dest='opt_has_one',
  220. help='one-to-one relationships of the form '
  221. '\'payment receipt\', where one payment has one '
  222. 'receipt', action='append', default=[])
  223. parser.add_argument('--namespace', default='', dest='opt_namespace',
  224. help='PHP namespace to create models classes in')
  225. parser.add_argument('--create-select', action='store_true',
  226. help='whether to create the \'select\' option in '
  227. 'relations', dest='opt_create_select')
  228. parser.add_argument('--create-accessible', dest='opt_create_accessible',
  229. help='whether to create the \'$attr_accessible\' '
  230. 'variable, containing all ' 'attributes except '
  231. 'primary or foreing keys', action='store_true')
  232. parser.add_argument('-a', '--create-all', action='store_true',
  233. help='create all available options and variables')
  234. parser.add_argument('-d', '--dir', help='directory to save model files in')
  235. parser.add_argument('-s', '--spaces', nargs='?', const=4, type=int,
  236. help='use spaces instead of tabs (4 by default)')
  237. args = parser.parse_args()
  238. print args
  239. options = {}
  240. for arg, value in args._get_kwargs():
  241. if arg[:4] == 'opt_':
  242. options[arg[4:]] = value
  243. if args.create_all:
  244. options['create_select'] = options['create_accessible'] = True
  245. if args.spaces:
  246. TAB = args.spaces * ' '
  247. conn = mysql.connect(args.host, args.user, args.password, args.dbname)
  248. models = create_models(conn, options)
  249. conn.close()
  250. if args.dir:
  251. if not os.path.exists(args.dir):
  252. os.makedirs(args.dir)
  253. for model in models:
  254. path = model.create_path_from_dir(args.dir)
  255. model.save_in_file(path)
  256. print 'Saved model %s in %s' % (model.classname(), path)
  257. else:
  258. for model in models:
  259. print model.generate_php()