PageRenderTime 79ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

/ase/db/app.py

https://gitlab.com/jennings.p.c/ase-f-ase
Python | 459 lines | 420 code | 19 blank | 20 comment | 10 complexity | ea10c3eaa41639a66164d25566816945 MD5 | raw file
  1. """WSGI Flask-app for browsing a database.
  2. You can launch Flask's local webserver like this::
  3. $ ase db abc.db -w
  4. For a real webserver, you need to set the $ASE_DB_APP_CONFIG environment
  5. variable to point to a configuration file like this::
  6. ASE_DB_NAMES = ['/path/to/db-file/project1.db',
  7. 'postgresql://user:pw@localhost:5432/project2']
  8. ASE_DB_HOMEPAGE = '<a href="https://home.page.dk">HOME</a> ::'
  9. Start with something like::
  10. twistd web --wsgi=ase.db.app.app --port=8000
  11. """
  12. from __future__ import print_function
  13. import collections
  14. import functools
  15. import io
  16. import os
  17. import os.path as op
  18. import re
  19. import sys
  20. import tempfile
  21. from flask import Flask, render_template, request, send_from_directory, flash
  22. try:
  23. import matplotlib
  24. matplotlib.use('Agg', warn=False)
  25. except ImportError:
  26. pass
  27. import ase.db
  28. import ase.db.web
  29. from ase.db.plot import atoms2png
  30. from ase.db.summary import Summary
  31. from ase.db.table import Table, all_columns
  32. from ase.visualize import view
  33. from ase import Atoms
  34. from ase.calculators.calculator import kptdensity2monkhorstpack
  35. from ase.utils import FileNotFoundError
  36. # Every client-connetions gets one of these tuples:
  37. Connection = collections.namedtuple(
  38. 'Connection',
  39. ['project', # project name
  40. 'query', # query string
  41. 'nrows', # number of rows matched
  42. 'page', # page number
  43. 'columns', # what columns to show
  44. 'sort', # what column to sort after
  45. 'limit']) # number of rows per page
  46. app = Flask(__name__)
  47. app.secret_key = 'asdf'
  48. databases = {}
  49. home = '' # link to homepage
  50. open_ase_gui = True # click image to open ASE's GUI
  51. # List of (project-name, title) tuples (will be filled in at run-time):
  52. projects = []
  53. def connect_databases(uris):
  54. python_configs = []
  55. dbs = []
  56. for uri in uris:
  57. if uri.endswith('.py'):
  58. python_configs.append(uri)
  59. continue
  60. if uri.startswith('postgresql://'):
  61. project = uri.rsplit('/', 1)[1]
  62. else:
  63. project = uri.rsplit('/', 1)[-1].split('.')[0]
  64. db = ase.db.connect(uri)
  65. db.python = None
  66. databases[project] = db
  67. dbs.append(db)
  68. for py, db in zip(python_configs, dbs):
  69. db.python = py
  70. next_con_id = 1
  71. connections = {}
  72. tmpdir = tempfile.mkdtemp() # used to cache png-files
  73. if 'ASE_DB_APP_CONFIG' in os.environ:
  74. app.config.from_envvar('ASE_DB_APP_CONFIG')
  75. connect_databases(app.config['ASE_DB_NAMES'])
  76. home = app.config['ASE_DB_HOMEPAGE']
  77. open_ase_gui = False
  78. try:
  79. os.unlink('tmpdir')
  80. except FileNotFoundError:
  81. pass
  82. os.symlink(tmpdir, 'tmpdir')
  83. # Find numbers in formulas so that we can convert H2O to H<sub>2</sub>O:
  84. SUBSCRIPT = re.compile(r'(\d+)')
  85. def database():
  86. return databases.get(request.args.get('project', 'default'))
  87. def prefix():
  88. if 'project' in request.args:
  89. return request.args['project'] + '-'
  90. return ''
  91. errors = 0
  92. def error(e):
  93. """Write traceback and other stuff to 00-99.error files."""
  94. global errors
  95. import traceback
  96. x = request.args.get('x', '0')
  97. try:
  98. cid = int(x)
  99. except ValueError:
  100. cid = 0
  101. con = connections.get(cid)
  102. with open(op.join(tmpdir, '{:02}.error'.format(errors % 100)), 'w') as fd:
  103. print(repr((errors, con, e, request)), file=fd)
  104. if hasattr(e, '__traceback__'):
  105. traceback.print_tb(e.__traceback__, file=fd)
  106. errors += 1
  107. raise e
  108. app.register_error_handler(Exception, error)
  109. @app.route('/')
  110. def index():
  111. global next_con_id
  112. if not projects:
  113. # First time: initialize list of projects
  114. for proj, db in sorted(databases.items()):
  115. meta = ase.db.web.process_metadata(db)
  116. db.meta = meta
  117. projects.append((proj, db.meta.get('title', proj)))
  118. con_id = int(request.args.get('x', '0'))
  119. if con_id in connections:
  120. project, query, nrows, page, columns, sort, limit = connections[con_id]
  121. newproject = request.args.get('project')
  122. if newproject is not None and newproject != project:
  123. con_id = 0
  124. if con_id not in connections:
  125. # Give this connetion a new id:
  126. con_id = next_con_id
  127. next_con_id += 1
  128. project = request.args.get('project', projects[0][0])
  129. query = ['', {}, '']
  130. nrows = None
  131. page = 0
  132. columns = None
  133. sort = 'id'
  134. limit = 25
  135. db = databases[project]
  136. meta = db.meta
  137. if columns is None:
  138. columns = meta.get('default_columns')[:] or list(all_columns)
  139. if 'sort' in request.args:
  140. column = request.args['sort']
  141. if column == sort:
  142. sort = '-' + column
  143. elif '-' + column == sort:
  144. sort = 'id'
  145. else:
  146. sort = column
  147. page = 0
  148. elif 'query' in request.args:
  149. dct = {}
  150. query = [request.args['query']]
  151. q = query[0]
  152. for special in meta['special_keys']:
  153. kind, key = special[:2]
  154. if kind == 'SELECT':
  155. value = request.args['select_' + key]
  156. dct[key] = value
  157. if value:
  158. q += ',{}={}'.format(key, value)
  159. elif kind == 'BOOL':
  160. value = request.args['bool_' + key]
  161. dct[key] = value
  162. if value:
  163. q += ',{}={}'.format(key, value)
  164. else:
  165. v1 = request.args['from_' + key]
  166. v2 = request.args['to_' + key]
  167. var = request.args['range_' + key]
  168. dct[key] = (v1, v2, var)
  169. if v1 or v2:
  170. var = request.args['range_' + key]
  171. if v1:
  172. q += ',{}>={}'.format(var, v1)
  173. if v2:
  174. q += ',{}<={}'.format(var, v2)
  175. q = q.lstrip(',')
  176. query += [dct, q]
  177. sort = 'id'
  178. page = 0
  179. nrows = None
  180. elif 'limit' in request.args:
  181. limit = int(request.args['limit'])
  182. page = 0
  183. elif 'page' in request.args:
  184. page = int(request.args['page'])
  185. if 'toggle' in request.args:
  186. column = request.args['toggle']
  187. if column == 'reset':
  188. columns = meta.get('default_columns')[:] or list(all_columns)
  189. else:
  190. if column in columns:
  191. columns.remove(column)
  192. if column == sort.lstrip('-'):
  193. sort = 'id'
  194. page = 0
  195. else:
  196. columns.append(column)
  197. okquery = query
  198. if nrows is None:
  199. try:
  200. nrows = db.count(query[2])
  201. except (ValueError, KeyError) as e:
  202. flash(', '.join(['Bad query'] + list(e.args)))
  203. okquery = ('', {}, 'id=0') # this will return no rows
  204. nrows = 0
  205. table = Table(db)
  206. table.select(okquery[2], columns, sort, limit, offset=page * limit)
  207. con = Connection(project, query, nrows, page, columns, sort, limit)
  208. connections[con_id] = con
  209. if len(connections) > 1000:
  210. # Forget old connections:
  211. for cid in sorted(connections)[:200]:
  212. del connections[cid]
  213. table.format(SUBSCRIPT)
  214. addcolumns = [column for column in all_columns + table.keys
  215. if column not in table.columns]
  216. return render_template('table.html',
  217. project=project,
  218. projects=projects,
  219. t=table,
  220. md=meta,
  221. con=con,
  222. x=con_id,
  223. home=home,
  224. pages=pages(page, nrows, limit),
  225. nrows=nrows,
  226. addcolumns=addcolumns,
  227. row1=page * limit + 1,
  228. row2=min((page + 1) * limit, nrows))
  229. @app.route('/image/<name>')
  230. def image(name):
  231. id = int(name[:-4])
  232. name = prefix() + name
  233. path = op.join(tmpdir, name)
  234. if not op.isfile(path):
  235. db = database()
  236. atoms = db.get_atoms(id)
  237. atoms2png(atoms, path)
  238. return send_from_directory(tmpdir, name)
  239. @app.route('/cif/<name>')
  240. def cif(name):
  241. id = int(name[:-4])
  242. name = prefix() + name
  243. path = op.join(tmpdir, name)
  244. if not op.isfile(path):
  245. db = database()
  246. atoms = db.get_atoms(id)
  247. atoms.write(path)
  248. return send_from_directory(tmpdir, name)
  249. @app.route('/plot/<png>')
  250. def plot(png):
  251. png = prefix() + png
  252. return send_from_directory(tmpdir, png)
  253. @app.route('/gui/<int:id>')
  254. def gui(id):
  255. if open_ase_gui:
  256. db = database()
  257. atoms = db.get_atoms(id)
  258. view(atoms)
  259. return '', 204, []
  260. @app.route('/id/<int:id>')
  261. def summary(id):
  262. db = database()
  263. if db is None:
  264. return ''
  265. if not hasattr(db, 'meta'):
  266. db.meta = ase.db.web.process_metadata(db)
  267. prfx = prefix() + str(id) + '-'
  268. row = db.get(id)
  269. s = Summary(row, db.meta, SUBSCRIPT, prfx, tmpdir)
  270. atoms = Atoms(cell=row.cell, pbc=row.pbc)
  271. n1, n2, n3 = kptdensity2monkhorstpack(atoms,
  272. kptdensity=1.8,
  273. even=False)
  274. return render_template('summary.html',
  275. project=request.args.get('project', 'default'),
  276. projects=projects,
  277. s=s,
  278. n1=n1,
  279. n2=n2,
  280. n3=n3,
  281. home=home,
  282. md=db.meta,
  283. open_ase_gui=open_ase_gui)
  284. def tofile(project, query, type, limit=0):
  285. fd, name = tempfile.mkstemp(suffix='.' + type)
  286. con = ase.db.connect(name, use_lock_file=False)
  287. db = databases[project]
  288. for row in db.select(query, limit=limit):
  289. con.write(row,
  290. data=row.get('data', {}),
  291. **row.get('key_value_pairs', {}))
  292. os.close(fd)
  293. data = open(name, 'rb').read()
  294. os.unlink(name)
  295. return data
  296. def download(f):
  297. @functools.wraps(f)
  298. def ff(*args, **kwargs):
  299. text, name = f(*args, **kwargs)
  300. headers = [('Content-Disposition',
  301. 'attachment; filename="{0}"'.format(name)),
  302. ] # ('Content-type', 'application/sqlite3')]
  303. return text, 200, headers
  304. return ff
  305. @app.route('/xyz/<int:id>')
  306. @download
  307. def xyz(id):
  308. fd = io.StringIO()
  309. from ase.io.xyz import write_xyz
  310. db = database()
  311. write_xyz(fd, db.get_atoms(id))
  312. data = fd.getvalue()
  313. return data, '{0}.xyz'.format(id)
  314. @app.route('/json')
  315. @download
  316. def jsonall():
  317. con_id = int(request.args['x'])
  318. con = connections[con_id]
  319. data = tofile(con.project, con.query[2], 'json', con.limit)
  320. return data, 'selection.json'
  321. @app.route('/json/<int:id>')
  322. @download
  323. def json1(id):
  324. project = request.args.get('project', 'default')
  325. data = tofile(project, id, 'json')
  326. return data, '{0}.json'.format(id)
  327. @app.route('/sqlite')
  328. @download
  329. def sqliteall():
  330. con_id = int(request.args['x'])
  331. con = connections[con_id]
  332. data = tofile(con.project, con.query[2], 'db', con.limit)
  333. return data, 'selection.db'
  334. @app.route('/sqlite/<int:id>')
  335. @download
  336. def sqlite1(id):
  337. project = request.args.get('project', 'default')
  338. data = tofile(project, id, 'db')
  339. return data, '{0}.db'.format(id)
  340. @app.route('/robots.txt')
  341. def robots():
  342. return ('User-agent: *\nDisallow: /\n\n' +
  343. 'User-agent: Baiduspider\nDisallow: /\n', 200)
  344. def pages(page, nrows, limit):
  345. """Helper function for pagination stuff."""
  346. npages = (nrows + limit - 1) // limit
  347. p1 = min(5, npages)
  348. p2 = max(page - 4, p1)
  349. p3 = min(page + 5, npages)
  350. p4 = max(npages - 4, p3)
  351. pgs = list(range(p1))
  352. if p1 < p2:
  353. pgs.append(-1)
  354. pgs += list(range(p2, p3))
  355. if p3 < p4:
  356. pgs.append(-1)
  357. pgs += list(range(p4, npages))
  358. pages = [(page - 1, 'previous')]
  359. for p in pgs:
  360. if p == -1:
  361. pages.append((-1, '...'))
  362. elif p == page:
  363. pages.append((-1, str(p + 1)))
  364. else:
  365. pages.append((p, str(p + 1)))
  366. nxt = min(page + 1, npages - 1)
  367. if nxt == page:
  368. nxt = -1
  369. pages.append((nxt, 'next'))
  370. return pages
  371. if __name__ == '__main__':
  372. if len(sys.argv) > 1:
  373. connect_databases(sys.argv[1:])
  374. open_ase_gui = False
  375. app.run(host='0.0.0.0', debug=True)