Coverage for /builds/ase/ase/ase/db/app.py : 85.98%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""WSGI Flask-app for browsing a database.
3::
5 +---------------------+
6 | layout.html |
7 | +-----------------+ | +--------------+
8 | | search.html | | | layout.html |
9 | | + | | | +---------+ |
10 | | table.html ----------->| |row.html | |
11 | | | | | +---------+ |
12 | +-----------------+ | +--------------+
13 +---------------------+
15You can launch Flask's local webserver like this::
17 $ ase db abc.db -w
19or this::
21 $ python3 -m ase.db.app abc.db
23"""
25import io
26import sys
27from typing import Dict, Any, Set
28from pathlib import Path
30from ase.db import connect
31from ase.db.core import Database
32from ase.formula import Formula
33from ase.db.web import create_key_descriptions, Session
34from ase.db.row import row2dct, AtomsRow
35from ase.db.table import all_columns
38def request2string(args) -> str:
39 """Converts request args to ase.db query string."""
40 return args['query']
43def row_to_dict(row: AtomsRow,
44 project: Dict[str, Any]) -> Dict[str, Any]:
45 """Convert row to dict for use in html template."""
46 dct = row2dct(row, project['key_descriptions'])
47 dct['formula'] = Formula(Formula(row.formula).format('abc')).format('html')
48 return dct
51class DBApp:
52 root = Path(__file__).parent.parent.parent
54 def __init__(self):
55 self.projects = {}
57 flask = new_app(self.projects)
58 self.flask = flask
60 @flask.route('/')
61 def frontpage():
62 projectname = next(iter(self.projects))
63 return flask.view_functions['search'](projectname)
65 def add_project(self, name: str, db: Database) -> None:
66 all_keys: Set[str] = set()
67 for row in db.select(columns=['key_value_pairs'], include_data=False):
68 all_keys.update(row._keys)
70 key_descriptions = {key: (key, '', '') for key in all_keys}
72 meta: Dict[str, Any] = db.metadata
74 if 'key_descriptions' in meta:
75 key_descriptions.update(meta['key_descriptions'])
77 default_columns = meta.get('default_columns')
78 if default_columns is None:
79 default_columns = all_columns[:]
81 self.projects[name] = {
82 'name': name,
83 'title': meta.get('title', ''),
84 'uid_key': 'id',
85 'key_descriptions': create_key_descriptions(key_descriptions),
86 'database': db,
87 'row_to_dict_function': row_to_dict,
88 'handle_query_function': request2string,
89 'default_columns': default_columns,
90 'search_template': 'ase/db/templates/search.html',
91 'row_template': 'ase/db/templates/row.html',
92 'table_template': 'ase/db/templates/table.html'}
94 @classmethod
95 def run_db(cls, db):
96 app = cls()
97 app.add_project('default', db)
98 app.flask.run(host='0.0.0.0', debug=True)
101def new_app(projects):
102 from flask import Flask, render_template, request
103 app = Flask(__name__, template_folder=str(DBApp.root))
105 @app.route('/<project_name>')
106 @app.route('/<project_name>/')
107 def search(project_name: str):
108 """Search page.
110 Contains input form for database query and a table result rows.
111 """
112 if project_name == 'favicon.ico':
113 return '', 204, [] # 204: "No content"
114 session = Session(project_name)
115 project = projects[project_name]
116 return render_template(project['search_template'],
117 q=request.args.get('query', ''),
118 p=project,
119 session_id=session.id)
121 @app.route('/update/<int:sid>/<what>/<x>/')
122 def update(sid: int, what: str, x: str):
123 """Update table of rows inside search page.
125 ``what`` must be one of:
127 * query: execute query in request.args (x not used)
128 * limit: set number of rows to show to x
129 * toggle: toggle column x
130 * sort: sort after column x
131 * page: show page x
132 """
133 session = Session.get(sid)
134 project = projects[session.project_name]
135 session.update(what, x, request.args, project)
136 table = session.create_table(project['database'],
137 project['uid_key'],
138 keys=list(project['key_descriptions']))
139 return render_template(project['table_template'],
140 t=table,
141 p=project,
142 s=session)
144 @app.route('/<project_name>/row/<uid>')
145 def row(project_name: str, uid: str):
146 """Show details for one database row."""
147 project = projects[project_name]
148 uid_key = project['uid_key']
149 row = project['database'].get('{uid_key}={uid}'
150 .format(uid_key=uid_key, uid=uid))
151 dct = project['row_to_dict_function'](row, project)
152 return render_template(project['row_template'],
153 d=dct, row=row, p=project, uid=uid)
155 @app.route('/atoms/<project_name>/<int:id>/<type>')
156 def atoms(project_name: str, id: int, type: str):
157 """Return atomic structure as cif, xyz or json."""
158 row = projects[project_name]['database'].get(id=id)
159 a = row.toatoms()
160 if type == 'cif':
161 b = io.BytesIO()
162 a.pbc = True
163 a.write(b, 'cif', wrap=False)
164 return b.getvalue(), 200, []
166 fd = io.StringIO()
167 if type == 'xyz':
168 a.write(fd, format='extxyz')
169 elif type == 'json':
170 con = connect(fd, type='json')
171 con.write(row,
172 data=row.get('data', {}),
173 **row.get('key_value_pairs', {}))
174 else:
175 1 / 0
177 headers = [('Content-Disposition',
178 'attachment; filename="{project_name}-{id}.{type}"'
179 .format(project_name=project_name, id=id, type=type))]
180 txt = fd.getvalue()
181 return txt, 200, headers
183 @app.route('/gui/<int:id>')
184 def gui(id: int):
185 """Pop ud ase gui window."""
186 from ase.visualize import view
187 # XXX so broken
188 arbitrary_project = next(iter(projects))
189 atoms = projects[arbitrary_project]['database'].get_atoms(id)
190 view(atoms)
191 return '', 204, []
193 @app.route('/test')
194 def test():
195 return 'hello, world!'
197 @app.route('/robots.txt')
198 def robots():
199 return ('User-agent: *\n'
200 'Disallow: /\n'
201 '\n'
202 'User-agent: Baiduspider\n'
203 'Disallow: /\n'
204 '\n'
205 'User-agent: SiteCheck-sitecrawl by Siteimprove.com\n'
206 'Disallow: /\n',
207 200)
209 return app
212handle_query = request2string
215def main():
216 db = connect(sys.argv[1])
217 DBApp.run_db(db)
220if __name__ == '__main__':
221 main()