Hide keyboard shortcuts

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"""Flask web application views.""" 

2import datetime 

3import platform 

4import re 

5import socket 

6import sys 

7 

8try: 

9 from importlib import metadata 

10except ImportError: 

11 # FIXME Remove when we drop support for Python < 3.7 

12 import importlib_metadata as metadata 

13 

14from astropy.time import Time 

15from flask import flash, jsonify, redirect, render_template, request, url_for 

16from flask import make_response 

17from requests.exceptions import HTTPError 

18 

19from . import app as celery_app 

20from ._version import get_versions 

21from .flask import app, cache 

22from .tasks import first2years, gracedb, orchestrator, circulars, superevents 

23from .util import PromiseProxy 

24 

25distributions = PromiseProxy(lambda: tuple(metadata.distributions())) 

26 

27 

28@app.route('/') 

29def index(): 

30 """Render main page.""" 

31 return render_template( 

32 'index.jinja2', 

33 conf=celery_app.conf, 

34 hostname=socket.getfqdn(), 

35 distributions=distributions, 

36 platform=platform.platform(), 

37 versions=get_versions(), 

38 python_version=sys.version) 

39 

40 

41def take_n(n, iterable): 

42 """Take the first `n` items of a collection.""" 

43 for i, item in enumerate(iterable): 

44 if i >= n: 

45 break 

46 yield item 

47 

48 

49# Regular expression for parsing query strings 

50# that look like GraceDB superevent names. 

51_typeahead_superevent_id_regex = re.compile( 

52 r'(?P<prefix>[MT]?)S?(?P<date>\d{0,6})(?P<suffix>[a-z]*)', 

53 re.IGNORECASE) 

54 

55 

56@app.route('/typeahead_superevent_id') 

57@cache.cached(query_string=True) 

58def typeahead_superevent_id(): 

59 """Search GraceDB for superevents by ID. 

60 

61 This involves some date parsing because GraceDB does not support directly 

62 searching for superevents by ID substring. 

63 """ 

64 max_results = 8 # maximum number of results to return 

65 batch_results = 32 # batch size for results from server 

66 

67 term = request.args.get('superevent_id') 

68 match = _typeahead_superevent_id_regex.fullmatch(term) if term else None 

69 

70 if match: 

71 # Determine GraceDB event category from regular expression. 

72 prefix = match['prefix'].upper() + 'S' 

73 category = {'T': 'test', 'M': 'MDC'}.get( 

74 match['prefix'].upper(), 'production') 

75 

76 # Determine start date from regular expression by padding out 

77 # the partial date with missing digits defaulting to 000101. 

78 date_partial = match['date'] 

79 date_partial_length = len(date_partial) 

80 try: 

81 date_start = datetime.datetime.strptime( 

82 date_partial + '000101'[date_partial_length:], '%y%m%d') 

83 except ValueError: # invalid date 

84 return jsonify([]) 

85 

86 # Determine end date from regular expression by adding a very 

87 # loose upper bound on the number of days until the next 

88 # digit in the date rolls over. No need to be exact here. 

89 date_end = date_start + datetime.timedelta( 

90 days=[36600, 3660, 366, 320, 32, 11, 1.1][date_partial_length]) 

91 

92 # Determine GraceDB event suffix from regular expression. 

93 suffix = match['suffix'].lower() 

94 else: 

95 prefix = 'S' 

96 category = 'production' 

97 date_end = datetime.datetime.utcnow() 

98 date_start = date_end - datetime.timedelta(days=7) 

99 date_partial = '' 

100 date_partial_length = 0 

101 suffix = '' 

102 

103 # Query GraceDB. 

104 query = 'category: {} t_0: {} .. {}'.format( 

105 category, Time(date_start).gps, Time(date_end).gps) 

106 response = gracedb.client.superevents.search( 

107 query=query, sort='superevent_id', count=batch_results) 

108 

109 # Filter superevent IDs that match the search term. 

110 regex = re.compile(r'{}{}\d{{{}}}{}[a-z]*'.format( 

111 prefix, date_partial, 6 - date_partial_length, suffix)) 

112 superevent_ids = ( 

113 superevent['superevent_id'] for superevent 

114 in response if regex.fullmatch(superevent['superevent_id'])) 

115 

116 # Return only the first few matches. 

117 return jsonify(list(take_n(max_results, superevent_ids))) 

118 

119 

120@app.route('/typeahead_event_id') 

121@cache.cached(query_string=True) 

122def typeahead_event_id(): 

123 """Search GraceDB for events by ID.""" 

124 superevent_id = request.args.get('superevent_id').strip() 

125 query_terms = [f'superevent: {superevent_id}'] 

126 if superevent_id.startswith('T'): 

127 query_terms.append('Test') 

128 elif superevent_id.startswith('M'): 

129 query_terms.append('MDC') 

130 query = ' '.join(query_terms) 

131 try: 

132 results = gracedb.get_events(query) 

133 except HTTPError: 

134 results = [] 

135 results = [dict(r, snr=superevents.get_snr(r)) for r in results 

136 if superevents.is_complete(r)] 

137 return jsonify(list(reversed(sorted(results, key=superevents.keyfunc)))) 

138 

139 

140def _search_by_tag_and_filename(superevent_id, filename, extension, tag): 

141 try: 

142 records = gracedb.get_log(superevent_id) 

143 return [ 

144 '{},{}'.format(record['filename'], record['file_version']) 

145 for record in records if tag in record['tag_names'] 

146 and record['filename'].startswith(filename) 

147 and record['filename'].endswith(extension)] 

148 except HTTPError as e: 

149 # Ignore 404 errors from server 

150 if e.response.status_code == 404: 

151 return [] 

152 else: 

153 raise 

154 

155 

156@app.route('/typeahead_skymap_filename') 

157@cache.cached(query_string=True) 

158def typeahead_skymap_filename(): 

159 """Search for sky maps by filename.""" 

160 return jsonify(_search_by_tag_and_filename( 

161 request.args.get('superevent_id') or '', 

162 request.args.get('filename') or '', 

163 '.fits.gz', 'sky_loc' 

164 )) 

165 

166 

167@app.route('/typeahead_em_bright_filename') 

168@cache.cached(query_string=True) 

169def typeahead_em_bright_filename(): 

170 """Search em_bright files by filename.""" 

171 return jsonify(_search_by_tag_and_filename( 

172 request.args.get('superevent_id') or '', 

173 request.args.get('filename') or '', 

174 '.json', 'em_bright' 

175 )) 

176 

177 

178@app.route('/typeahead_p_astro_filename') 

179@cache.cached(query_string=True) 

180def typeahead_p_astro_filename(): 

181 """Search p_astro files by filename.""" 

182 return jsonify(_search_by_tag_and_filename( 

183 request.args.get('superevent_id') or '', 

184 request.args.get('filename') or '', 

185 '.json', 'p_astro' 

186 )) 

187 

188 

189@app.route('/send_preliminary_gcn', methods=['POST']) 

190def send_preliminary_gcn(): 

191 """Handle submission of preliminary alert form.""" 

192 keys = ('superevent_id', 'event_id') 

193 superevent_id, event_id, *_ = tuple(request.form.get(key) for key in keys) 

194 if superevent_id and event_id: 

195 ( 

196 gracedb.upload.s( 

197 None, None, superevent_id, 

198 'User {} queued a Preliminary alert through the dashboard.' 

199 .format(request.remote_user or '(unknown)'), 

200 tags=['em_follow']) 

201 | 

202 gracedb.update_superevent.si( 

203 superevent_id, preferred_event=event_id) 

204 | 

205 gracedb.get_event.si(event_id) 

206 | 

207 orchestrator.preliminary_alert.s(superevent_id) 

208 ).delay() 

209 flash('Queued preliminary alert for {}.'.format(superevent_id), 

210 'success') 

211 else: 

212 flash('No alert sent. Please fill in all fields.', 'danger') 

213 return redirect(url_for('index')) 

214 

215 

216@app.route('/change_prefered_event', methods=['POST']) 

217def change_prefered_event(): 

218 """Handle submission of preliminary alert form.""" 

219 keys = ('superevent_id', 'event_id') 

220 superevent_id, event_id, *_ = tuple(request.form.get(key) for key in keys) 

221 if superevent_id and event_id: 

222 ( 

223 gracedb.upload.s( 

224 None, None, superevent_id, 

225 'User {} queued a prefered event change to {}.' 

226 .format(request.remote_user or '(unknown)', event_id), 

227 tags=['em_follow']) 

228 | 

229 gracedb.update_superevent.si( 

230 superevent_id, preferred_event=event_id) 

231 | 

232 gracedb.get_event.si(event_id) 

233 | 

234 orchestrator.preliminary_alert.s( 

235 superevent_id, initiate_voevent=False) 

236 ).delay() 

237 flash('Changed prefered event for {}.'.format(superevent_id), 

238 'success') 

239 else: 

240 flash('No change performed. Please fill in all fields.', 'danger') 

241 return redirect(url_for('index')) 

242 

243 

244@app.route('/send_update_gcn', methods=['POST']) 

245def send_update_gcn(): 

246 """Handle submission of update alert form.""" 

247 keys = ('superevent_id', 'skymap_filename', 

248 'em_bright_filename', 'p_astro_filename') 

249 superevent_id, *filenames = args = tuple( 

250 request.form.get(key) for key in keys) 

251 if all(args): 

252 ( 

253 gracedb.upload.s( 

254 None, None, superevent_id, 

255 'User {} queued an Update alert through the dashboard.' 

256 .format(request.remote_user or '(unknown)'), 

257 tags=['em_follow']) 

258 | 

259 orchestrator.update_alert.si(filenames, superevent_id) 

260 ).delay() 

261 flash('Queued update alert for {}.'.format(superevent_id), 'success') 

262 else: 

263 flash('No alert sent. Please fill in all fields.', 'danger') 

264 return redirect(url_for('index')) 

265 

266 

267@app.route('/create_update_gcn_circular', methods=['POST']) 

268def create_update_gcn_circular(): 

269 """Handle submission of GCN Circular form.""" 

270 keys = ['sky_localization', 'em_bright', 'p_astro'] 

271 superevent_id = request.form.get('superevent_id') 

272 updates = [key for key in keys if request.form.get(key)] 

273 if superevent_id and updates: 

274 response = make_response(circulars.create_update_circular( 

275 superevent_id, 

276 update_types=updates)) 

277 response.headers["content-type"] = "text/plain" 

278 return response 

279 else: 

280 flash('No circular created. Please fill in superevent ID and at ' + 

281 'least one update type.', 'danger') 

282 return redirect(url_for('index')) 

283 

284 

285@app.route('/send_mock_event', methods=['POST']) 

286def send_mock_event(): 

287 """Handle submission of mock alert form.""" 

288 first2years.upload_event.delay() 

289 flash('Queued a mock event.', 'success') 

290 return redirect(url_for('index'))