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

86 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3"""WSGI Flask-app for browsing a database. 

4 

5:: 

6 

7 +---------------------+ 

8 | layout.html | 

9 | +-----------------+ | +--------------+ 

10 | | search.html | | | layout.html | 

11 | | + | | | +---------+ | 

12 | | table.html ----------->| |row.html | | 

13 | | | | | +---------+ | 

14 | +-----------------+ | +--------------+ 

15 +---------------------+ 

16 

17You can launch Flask's local webserver like this:: 

18 

19 $ ase db abc.db -w 

20 

21or this:: 

22 

23 $ python3 -m ase.db.app abc.db 

24 

25""" 

26 

27import io 

28import sys 

29from pathlib import Path 

30 

31from ase.db import connect 

32from ase.db.core import Database 

33from ase.db.project import DatabaseProject 

34from ase.db.web import Session 

35 

36 

37class DBApp: 

38 root = Path(__file__).parent.parent.parent 

39 

40 def __init__(self): 

41 self.projects = {} 

42 

43 flask = new_app(self.projects) 

44 self.flask = flask 

45 

46 # Other projects will want to control the routing of the front 

47 # page, for which reasons we route it here in DBApp instead of 

48 # already in new_app(). 

49 @flask.route('/') 

50 def frontpage(): 

51 projectname = next(iter(self.projects)) 

52 return flask.view_functions['search'](projectname) 

53 

54 def add_project(self, name: str, db: Database) -> None: 

55 self.projects[name] = DatabaseProject.load_db_as_ase_project( 

56 name=name, database=db) 

57 

58 @classmethod 

59 def run_db(cls, db): 

60 app = cls() 

61 app.add_project('default', db) 

62 app.flask.run(host='0.0.0.0', debug=True) 

63 

64 

65def new_app(projects): 

66 from flask import Flask, render_template, request 

67 app = Flask(__name__, template_folder=str(DBApp.root)) 

68 

69 @app.route('/<project_name>') 

70 @app.route('/<project_name>/') 

71 def search(project_name: str): 

72 """Search page. 

73 

74 Contains input form for database query and a table result rows. 

75 """ 

76 if project_name == 'favicon.ico': 

77 return '', 204, [] # 204: "No content" 

78 session = Session(project_name) 

79 project = projects[project_name] 

80 return render_template(str(project.get_search_template()), 

81 q=request.args.get('query', ''), 

82 project=project, 

83 session_id=session.id) 

84 

85 @app.route('/update/<int:sid>/<what>/<x>/') 

86 def update(sid: int, what: str, x: str): 

87 """Update table of rows inside search page. 

88 

89 ``what`` must be one of: 

90 

91 * query: execute query in request.args (x not used) 

92 * limit: set number of rows to show to x 

93 * toggle: toggle column x 

94 * sort: sort after column x 

95 * page: show page x 

96 """ 

97 session = Session.get(sid) 

98 project = projects[session.project_name] 

99 session.update(what, x, request.args, project) 

100 table = session.create_table(project.database, 

101 project.uid_key, 

102 keys=list(project.key_descriptions)) 

103 return render_template(str(project.get_table_template()), 

104 table=table, 

105 project=project, 

106 session=session) 

107 

108 @app.route('/<project_name>/row/<uid>') 

109 def row(project_name: str, uid: str): 

110 """Show details for one database row.""" 

111 project = projects[project_name] 

112 row = project.uid_to_row(uid) 

113 dct = project.row_to_dict(row) 

114 return render_template(str(project.get_row_template()), 

115 dct=dct, row=row, project=project, uid=uid) 

116 

117 @app.route('/atoms/<project_name>/<int:id>/<type>') 

118 def atoms(project_name: str, id: int, type: str): 

119 """Return atomic structure as cif, xyz or json.""" 

120 row = projects[project_name].database.get(id=id) 

121 a = row.toatoms() 

122 if type == 'cif': 

123 b = io.BytesIO() 

124 a.pbc = True 

125 a.write(b, 'cif', wrap=False) 

126 return b.getvalue(), 200, [] 

127 

128 fd = io.StringIO() 

129 if type == 'xyz': 

130 a.write(fd, format='extxyz') 

131 elif type == 'json': 

132 con = connect(fd, type='json') 

133 con.write(row, 

134 data=row.get('data', {}), 

135 **row.get('key_value_pairs', {})) 

136 else: 

137 1 / 0 

138 

139 headers = [('Content-Disposition', 

140 'attachment; filename="{project_name}-{id}.{type}"' 

141 .format(project_name=project_name, id=id, type=type))] 

142 txt = fd.getvalue() 

143 return txt, 200, headers 

144 

145 @app.route('/gui/<int:id>') 

146 def gui(id: int): 

147 """Pop ud ase gui window.""" 

148 from ase.visualize import view 

149 

150 # XXX so broken 

151 arbitrary_project = next(iter(projects)) 

152 atoms = projects[arbitrary_project].database.get_atoms(id) 

153 view(atoms) 

154 return '', 204, [] 

155 

156 @app.route('/test') 

157 def test(): 

158 return 'hello, world!' 

159 

160 @app.route('/robots.txt') 

161 def robots(): 

162 return ('User-agent: *\n' 

163 'Disallow: /\n' 

164 '\n' 

165 'User-agent: Baiduspider\n' 

166 'Disallow: /\n' 

167 '\n' 

168 'User-agent: SiteCheck-sitecrawl by Siteimprove.com\n' 

169 'Disallow: /\n', 

170 200) 

171 

172 return app 

173 

174 

175def main(): 

176 db = connect(sys.argv[1]) 

177 DBApp.run_db(db) 

178 

179 

180if __name__ == '__main__': 

181 main()