def render_GET(self, request): subject = resourceutil.getSubject(request) # authz check ctx = [ (rights.CTX_VIEWGROUP, 'wlcg') ] if not self.authorizer.isAllowed(subject, rights.ACTION_VIEW, ctx): return self.renderAuthzErrorPage(request, 'WLCG oversight view', subject) # access allowed start_date, end_date = dateform.parseStartEndDates(request) # set dates if not specified (defaults are current month, which is not what we want) year, quart = dateform.currentYearQuart() if not 'startdate' in request.args or request.args['startdate'] == ['']: start_date, _ = dateform.quarterStartEndDates(year, quart) if not 'enddate' in request.args or request.args['enddate'] == ['']: _, end_date = dateform.quarterStartEndDates(year, quart) if 'unit' in request.args and request.args['unit'][0] not in WLCG_UNIT_MAPPING: return self.renderErrorPage('Invalid units parameters') unit = request.args.get('unit', ['ksi2k-ne'])[0] t_query_start = time.time() d = self.retrieveWLCGData(start_date, end_date) d.addCallback(self.renderWLCGViewPage, request, start_date, end_date, unit, t_query_start) d.addErrback(self.renderErrorPage, request) return server.NOT_DONE_YET
def render_GET(self, request): subject = resourceutil.getSubject(request) # authz check ctx = [ (rights.CTX_VIEWGROUP, 'wlcg') ] if not self.authorizer.isAllowed(subject, rights.ACTION_VIEW, ctx): return self.renderAuthzErrorPage(request, 'WLCG oversight view', subject) # access allowed start_date, end_date = dateform.parseStartEndDates(request) # set dates if not specified (defaults are current month, which is not what we want) year, quart = dateform.currentYearQuart() if not 'startdate' in request.args or request.args['startdate'] == ['']: start_date, _ = dateform.quarterStartEndDates(year, quart) if not 'enddate' in request.args or request.args['enddate'] == ['']: _, end_date = dateform.quarterStartEndDates(year, quart) if 'unit' in request.args and request.args['unit'][0] not in WLCG_UNIT_MAPPING: return self.renderErrorPage('Invalid units parameters') unit = request.args.get('unit', ['ksi2k-ne'])[0] t_query_start = time.time() d = self.retrieveWLCGData(start_date, end_date) d.addCallback(self.renderWLCGViewPage, request, start_date, end_date, unit, t_query_start) d.addErrback(self.renderErrorPage, request) return server.NOT_DONE_YET
def renderWLCGViewPage(self, wlcg_data, request, start_date, end_date, unit, t_query_start): t_query = time.time() - t_query_start days = dateform.dayDelta(start_date, end_date) t_dataprocess_start = time.time() wlcg_records = dataprocess.rowsToDicts(wlcg_data) # information on ops and dteam vo does not add any value wlcg_records = [ rec for rec in wlcg_records if rec[dataprocess.VO_NAME] not in ('dteam', 'ops') ] # massage data wlcg_records = dataprocess.addMissingScaleValues(wlcg_records) wlcg_records = dataprocess.collapseFields(wlcg_records, self.collapse) wlcg_records = dataprocess.tierMergeSplit(wlcg_records, self.tier_mapping, self.tier_shares, self.default_tier) # role must be collapsed after split in order for the tier split to function wlcg_records = dataprocess.collapseFields(wlcg_records, [ dataprocess.VO_ROLE ] ) sort_key = lambda key : dataprocess.sortKey(key, field_order=[ dataprocess.HOST] ) split_records = dataprocess.splitRecords(wlcg_records, dataprocess.TIER) for split_attr, records in split_records.items(): split_records[split_attr] = sorted(records, key=sort_key) t_dataprocess = time.time() - t_dataprocess_start # get tld groups hosts = set( [ rec[dataprocess.HOST] for rec in wlcg_records ] ) tld_groups = {} for host in hosts: tld = host.split('.')[-1].upper() tld_groups.setdefault(tld, []).append(host) # create composite to-tier names vo_tiers = set() for rec in wlcg_records: tl = 't1' if 'T1' in rec[dataprocess.TIER] else 't2' vt = tl + '-' + rec[dataprocess.VO_NAME] rec[dataprocess.VO_NAME] = vt del rec[dataprocess.TIER] # same as collapsing afterwards vo_tiers.add(vt) TOTAL = 'Total' TIER_TOTAL = self.default_tier.split('-')[0].upper() # calculate total per site site_totals = dataprocess.collapseFields(wlcg_records, ( dataprocess.VO_NAME, ) ) for r in site_totals: r[dataprocess.VO_NAME] = TOTAL # calculate total per country-tier country_tier_totals = [ r.copy() for r in wlcg_records ] for rec in country_tier_totals: rec[dataprocess.HOST] = rec[dataprocess.HOST].split('.')[-1].upper() + '-TOTAL' rec[dataprocess.USER] = 'FAKE' country_tier_totals = dataprocess.collapseFields(country_tier_totals, ( dataprocess.USER, ) ) # calculate total per country country_totals = dataprocess.collapseFields(country_tier_totals, ( dataprocess.VO_NAME, ) ) for rec in country_totals: rec[dataprocess.VO_NAME] = TOTAL # calculate total per tier-vo tier_vo_totals = dataprocess.collapseFields(wlcg_records, ( dataprocess.HOST, ) ) for r in tier_vo_totals: r[dataprocess.HOST] = TIER_TOTAL # calculate total total = dataprocess.collapseFields(wlcg_records, ( dataprocess.HOST, dataprocess.VO_NAME ) ) assert len(total) in (0,1), 'Records did not collapse into a single record when calculating grand total' if len(total) == 0: total = [ { dataprocess.CPU_TIME : 0, dataprocess.WALL_TIME : 0, dataprocess.KSI2K_CPU_TIME : 0, dataprocess.KSI2K_WALL_TIME : 0 } ] total_record = total[0] total_record[dataprocess.HOST] = TIER_TOTAL total_record[dataprocess.VO_NAME] = TOTAL # put all calculated records together and add equivalents wlcg_records += site_totals wlcg_records += country_tier_totals wlcg_records += country_totals wlcg_records += tier_vo_totals wlcg_records += [ total_record ] wlcg_records = dataprocess.addEquivalentProperties(wlcg_records, days) # create table columns = sorted(vo_tiers) columns.append(TOTAL) row_names = [] for tld in sorted(tld_groups): row_names += tld_groups[tld] row_names.append(tld + '-TOTAL') row_names.append(TIER_TOTAL) unit_extractor = WLCG_UNIT_MAPPING.get(unit, WLCG_UNIT_MAPPING_DEFAULT) elements = [] for row in row_names: for col in columns: for rec in wlcg_records: if rec[dataprocess.HOST] == row and rec[dataprocess.VO_NAME] == col: value = _formatValue( unit_extractor(rec) ) # hurrah for formatting if row == TIER_TOTAL and col == TOTAL: value = htmltable.StyledTableValue(value, bold=True, double_underlined=True) elif (row.endswith('-TOTAL') and col == TOTAL) or row == TIER_TOTAL: value = htmltable.StyledTableValue(value, bold=True, underlined=True) elif row.endswith('-TOTAL') or row == TIER_TOTAL or col == TOTAL: value = htmltable.StyledTableValue(value, bold=True) elements.append( ((col,row), value)) break else: elements.append( ((col,row), '') ) matrix = dict(elements) table_content = htmltable.createHTMLTable(matrix, columns, row_names, column_labels=COLUMN_NAMES) # render page start_date_option = request.args.get('startdate', [''])[0] end_date_option = request.args.get('enddate', [''])[0] title = 'WLCG oversight view' unit_options = [ ( 'ksi2k-ne', 'KSI2K Node Equivalents (default)') , ( 'hs06-ne', 'HS06 Node Equivalents' ), ( 'ksi2k-wallhours', 'KSI2K Walltime Hours' ) , ( 'hs06-wallhours', 'HS06 Walltime hours') ] unit_buttons = html.createRadioButtons('unit', unit_options, checked_value=unit) selector_form = dateform.createMonthSelectorForm(self.path, start_date_option, end_date_option, unit_buttons) quarters = dateform.generateFormQuarters() quarter_links = [] for q in quarters: year, quart = dateform.parseQuarter(q) sd, ed = dateform.quarterStartEndDates(year, quart) quarter_links.append(html.createLink('%s?startdate=%s&enddate=%s' % (self.path, sd, ed), q ) ) range_text = html.createParagraph('Date range: %s - %s (%s days)' % (start_date, end_date, days)) request.write( html.HTML_VIEWBASE_HEADER % {'title': title} ) request.write( html.createTitle(title) ) request.write( html.createParagraph('Quarters: \n ' + (' ' + html.NBSP).join(quarter_links) ) ) request.write( html.SECTION_BREAK ) request.write( html.createParagraph(selector_form) ) request.write( html.SECTION_BREAK ) request.write( html.createParagraph(range_text) ) request.write( table_content ) request.write( html.SECTION_BREAK ) request.write( html.createParagraph('Query time: %s' % round(t_query, 2)) ) request.write( html.createParagraph('Data process time: %s' % round(t_dataprocess, 2)) ) request.write( html.HTML_VIEWBASE_FOOTER ) request.finish() return server.NOT_DONE_YET
def renderWLCGViewPage(self, wlcg_data, request, start_date, end_date, unit, t_query_start): t_query = time.time() - t_query_start days = dateform.dayDelta(start_date, end_date) t_dataprocess_start = time.time() wlcg_records = dataprocess.rowsToDicts(wlcg_data) # information on ops and dteam vo does not add any value wlcg_records = [ rec for rec in wlcg_records if rec[dataprocess.VO_NAME] not in ('dteam', 'ops') ] # massage data wlcg_records = dataprocess.addMissingScaleValues(wlcg_records) wlcg_records = dataprocess.collapseFields(wlcg_records, self.collapse) wlcg_records = dataprocess.tierMergeSplit(wlcg_records, self.tier_mapping, self.tier_shares, self.default_tier) # role must be collapsed after split in order for the tier split to function wlcg_records = dataprocess.collapseFields(wlcg_records, [ dataprocess.VO_ROLE ] ) sort_key = lambda key : dataprocess.sortKey(key, field_order=[ dataprocess.HOST] ) split_records = dataprocess.splitRecords(wlcg_records, dataprocess.TIER) for split_attr, records in split_records.items(): split_records[split_attr] = sorted(records, key=sort_key) t_dataprocess = time.time() - t_dataprocess_start # get tld groups hosts = set( [ rec[dataprocess.HOST] for rec in wlcg_records ] ) tld_groups = {} for host in hosts: tld = host.split('.')[-1].upper() tld_groups.setdefault(tld, []).append(host) # create composite to-tier names vo_tiers = set() for rec in wlcg_records: tl = 't1' if 'T1' in rec[dataprocess.TIER] else 't2' vt = tl + '-' + rec[dataprocess.VO_NAME] rec[dataprocess.VO_NAME] = vt del rec[dataprocess.TIER] # same as collapsing afterwards vo_tiers.add(vt) TOTAL = 'Total' TIER_TOTAL = self.default_tier.split('-')[0].upper() # calculate total per site site_totals = dataprocess.collapseFields(wlcg_records, ( dataprocess.VO_NAME, ) ) for r in site_totals: r[dataprocess.VO_NAME] = TOTAL # calculate total per country-tier country_tier_totals = [ r.copy() for r in wlcg_records ] for rec in country_tier_totals: rec[dataprocess.HOST] = rec[dataprocess.HOST].split('.')[-1].upper() + '-TOTAL' rec[dataprocess.USER] = 'FAKE' country_tier_totals = dataprocess.collapseFields(country_tier_totals, ( dataprocess.USER, ) ) # calculate total per country country_totals = dataprocess.collapseFields(country_tier_totals, ( dataprocess.VO_NAME, ) ) for rec in country_totals: rec[dataprocess.VO_NAME] = TOTAL # calculate total per tier-vo tier_vo_totals = dataprocess.collapseFields(wlcg_records, ( dataprocess.HOST, ) ) for r in tier_vo_totals: r[dataprocess.HOST] = TIER_TOTAL # calculate total total = dataprocess.collapseFields(wlcg_records, ( dataprocess.HOST, dataprocess.VO_NAME ) ) assert len(total) in (0,1), 'Records did not collapse into a single record when calculating grand total' if len(total) == 0: total = [ { dataprocess.CPU_TIME : 0, dataprocess.WALL_TIME : 0, dataprocess.KSI2K_CPU_TIME : 0, dataprocess.KSI2K_WALL_TIME : 0 } ] total_record = total[0] total_record[dataprocess.HOST] = TIER_TOTAL total_record[dataprocess.VO_NAME] = TOTAL # put all calculated records together and add equivalents wlcg_records += site_totals wlcg_records += country_tier_totals wlcg_records += country_totals wlcg_records += tier_vo_totals wlcg_records += [ total_record ] wlcg_records = dataprocess.addEquivalentProperties(wlcg_records, days) # create table columns = sorted(vo_tiers) columns.append(TOTAL) row_names = [] for tld in sorted(tld_groups): row_names += tld_groups[tld] row_names.append(tld + '-TOTAL') row_names.append(TIER_TOTAL) unit_extractor = WLCG_UNIT_MAPPING.get(unit, WLCG_UNIT_MAPPING_DEFAULT) elements = [] for row in row_names: for col in columns: for rec in wlcg_records: if rec[dataprocess.HOST] == row and rec[dataprocess.VO_NAME] == col: value = _formatValue( unit_extractor(rec) ) # hurrah for formatting if row == TIER_TOTAL and col == TOTAL: value = htmltable.StyledTableValue(value, bold=True, double_underlined=True) elif (row.endswith('-TOTAL') and col == TOTAL) or row == TIER_TOTAL: value = htmltable.StyledTableValue(value, bold=True, underlined=True) elif row.endswith('-TOTAL') or row == TIER_TOTAL or col == TOTAL: value = htmltable.StyledTableValue(value, bold=True) elements.append( ((col,row), value)) break else: elements.append( ((col,row), '') ) matrix = dict(elements) table_content = htmltable.createHTMLTable(matrix, columns, row_names, column_labels=COLUMN_NAMES) # render page start_date_option = request.args.get('startdate', [''])[0] end_date_option = request.args.get('enddate', [''])[0] title = 'WLCG oversight view' unit_options = [ ( 'ksi2k-ne', 'KSI2K Node Equivalents (default)') , ( 'hs06-ne', 'HS06 Node Equivalents' ), ( 'ksi2k-wallhours', 'KSI2K Walltime Hours' ) , ( 'hs06-wallhours', 'HS06 Walltime hours') ] unit_buttons = html.createRadioButtons('unit', unit_options, checked_value=unit) selector_form = dateform.createMonthSelectorForm(self.path, start_date_option, end_date_option, unit_buttons) quarters = dateform.generateFormQuarters() quarter_links = [] for q in quarters: year, quart = dateform.parseQuarter(q) sd, ed = dateform.quarterStartEndDates(year, quart) quarter_links.append(html.createLink('%s?startdate=%s&enddate=%s' % (self.path, sd, ed), q ) ) range_text = html.createParagraph('Date range: %s - %s (%s days)' % (start_date, end_date, days)) request.write( html.HTML_VIEWBASE_HEADER % {'title': title} ) request.write( html.createTitle(title) ) request.write( html.createParagraph('Quarters: \n ' + (' ' + html.NBSP).join(quarter_links) ) ) request.write( html.SECTION_BREAK ) request.write( html.createParagraph(selector_form) ) request.write( html.SECTION_BREAK ) request.write( html.createParagraph(range_text) ) request.write( table_content ) request.write( html.SECTION_BREAK ) request.write( html.createParagraph('Query time: %s' % round(t_query, 2)) ) request.write( html.createParagraph('Data process time: %s' % round(t_dataprocess, 2)) ) request.write( html.HTML_VIEWBASE_FOOTER ) request.finish() return server.NOT_DONE_YET
def renderWLCGViewPage(self, wlcg_data, request, start_date, end_date, unit, t_query_start): t_query = time.time() - t_query_start days = dateform.dayDelta(start_date, end_date) t_dataprocess_start = time.time() wlcg_records = dataprocess.rowsToDicts(wlcg_data) wlcg_records = [ r for r in wlcg_records if r[dataprocess.VO_NAME] in ('alice', 'atlas') ] # Separate the records into computing and storage comp_records = [] storage_records = [] for r in wlcg_records: if r[dataprocess.HOST] == 'STORAGE': storage_records.append(r) else: comp_records.append(r) # massage data comp_records = dataprocess.addMissingScaleValues( comp_records, self.hepspec06) comp_records = dataprocess.collapseFields(comp_records, self.collapse) comp_records = dataprocess.tierMergeSplit(comp_records, self.tier_mapping, self.tier_shares, self.default_tier) # Collapse the fields that couldn't be collapsed before the tier-splitting. comp_records = [r for r in comp_records if r['tier'] == u'NDGF-T1'] comp_records = dataprocess.collapseFields( comp_records, [dataprocess.VO_ROLE, dataprocess.HOST]) summary = {} for r in comp_records + storage_records: vo = r[dataprocess.VO_NAME] if vo not in summary: summary[vo] = { 'disk': 0.0, 'tape': 0.0, dataprocess.HS06_WALL_TIME: 0.0, dataprocess.HS06_CPU_TIME: 0.0 } if r.get(dataprocess.HOST, '') == 'STORAGE': media = r[ 'vo_group'] # We stored "storage_media" in "vo_group" ... summary[vo][media] += r[ dataprocess. N_JOBS] # ... and "resource_capacity_used" in "n_jobs". else: summary[vo][dataprocess.HS06_WALL_TIME] += r[ dataprocess.HS06_WALL_TIME] summary[vo][dataprocess.HS06_CPU_TIME] += r[ dataprocess.HS06_CPU_TIME] columns = [("Walltime (HS06 days)", lambda r: r[dataprocess.HS06_WALL_TIME] / 24.0), ("CPUtime (HS06 days)", lambda r: r[dataprocess.HS06_CPU_TIME] / 24.0), ("Disk (TiB)", lambda r: r['disk'] / 1024.0**4), ("Tape (TiB)", lambda r: r['tape'] / 1024.0**4)] elements = [] for row in summary.keys(): for cname, cfunc in columns: value = "%.2f" % cfunc(summary[row]) elements.append(((cname, row), value)) t_dataprocess = time.time() - t_dataprocess_start matrix = dict(elements) table_content = htmltable.createHTMLTable(matrix, [c[0] for c in columns], summary.keys()) # render page start_date_option = request.args.get('startdate', [''])[0] end_date_option = request.args.get('enddate', [''])[0] title = 'WLCG T1 Summary' selector_form = dateform.createMonthSelectorForm( self.path, start_date_option, end_date_option) quarters = dateform.generateFormQuarters() quarter_links = [] for q in quarters: year, quart = dateform.parseQuarter(q) sd, ed = dateform.quarterStartEndDates(year, quart) quarter_links.append( html.createLink( '%s?startdate=%s&enddate=%s' % (self.path, sd, ed), q)) range_text = html.createParagraph('Date range: %s - %s (%s days)' % (start_date, end_date, days)) request.write(html.HTML_VIEWBASE_HEADER % {'title': title}) request.write(html.createTitle(title)) request.write( html.createParagraph('Quarters: \n ' + (' ' + html.NBSP).join(quarter_links))) request.write(html.SECTION_BREAK) request.write(html.createParagraph(selector_form)) request.write(html.SECTION_BREAK) request.write(html.createParagraph(range_text)) request.write(table_content) request.write(html.SECTION_BREAK) request.write( html.createParagraph('Query time: %s' % round(t_query, 2))) request.write( html.createParagraph('Data process time: %s' % round(t_dataprocess, 2))) request.write(html.HTML_VIEWBASE_FOOTER) request.finish() return server.NOT_DONE_YET
def renderWLCGViewPage(self, wlcg_data, request, start_date, end_date, unit, t_query_start): t_query = time.time() - t_query_start days = dateform.dayDelta(start_date, end_date) t_dataprocess_start = time.time() wlcg_records = wlcg.rowsToDicts(wlcg_data, [ wlcg.MACHINE_NAME, wlcg.COUNTRY, wlcg.VO_NAME, unit ]) t_dataprocess = time.time() - t_dataprocess_start # get tld groups tld_groups = {} for rec in wlcg_records: host = rec[wlcg.MACHINE_NAME] tld = _countryCode(rec) if tld not in tld_groups: tld_groups[tld] = [host] elif host not in tld_groups[tld]: tld_groups[tld].append(host) vo_tiers = set() for rec in wlcg_records: vo_tiers.add(rec[wlcg.VO_NAME]) TOTAL = 'Total' TIER_TOTAL = self.default_tier.split('-')[0].upper() site_totals = _collapseFields(wlcg_records, ( wlcg.VO_NAME, ) ) for r in site_totals: r[wlcg.VO_NAME] = TOTAL country_vo_aggregate = {} for r in wlcg_records: machine = _countryCode(r) + '-TOTAL' if machine not in country_vo_aggregate: country_vo_aggregate[machine] = {} vo = r[wlcg.VO_NAME] if vo not in country_vo_aggregate[machine]: country_vo_aggregate[machine][vo] = 0 country_vo_aggregate[machine][vo] += r[unit] country_tier_totals = [] for machine in country_vo_aggregate: for vo in country_vo_aggregate[machine]: r = {wlcg.MACHINE_NAME: machine, wlcg.VO_NAME: vo, unit: country_vo_aggregate[machine][vo]} country_tier_totals.append(r) # calculate total per country country_totals = _collapseFields(country_tier_totals, ( wlcg.VO_NAME, ) ) for rec in country_totals: rec[wlcg.VO_NAME] = TOTAL # calculate total per tier-vo tier_vo_totals = _collapseFields(wlcg_records, ( wlcg.MACHINE_NAME, wlcg.COUNTRY ) ) for r in tier_vo_totals: r[wlcg.MACHINE_NAME] = TIER_TOTAL # calculate total total = _collapseFields(wlcg_records, ( wlcg.MACHINE_NAME, wlcg.VO_NAME, wlcg.COUNTRY ) ) assert len(total) in (0,1), 'Records did not collapse into a single record when calculating grand total' if len(total) == 0: total = [ { wlcg.CPU_SECONDS : 0, wlcg.CORE_SECONDS : 0, wlcg.CPU_SECONDS_HS06 : 0, wlcg.CORE_SECONDS_HS06 : 0 } ] total_record = total[0] total_record[wlcg.MACHINE_NAME] = TIER_TOTAL total_record[wlcg.VO_NAME] = TOTAL # put all calculated records together and add equivalents wlcg_records += site_totals wlcg_records += country_tier_totals wlcg_records += country_totals wlcg_records += tier_vo_totals wlcg_records += [ total_record ] # create table columns = sorted(vo_tiers) columns.append(TOTAL) row_names = [] for tld in sorted(tld_groups): row_names += sorted(tld_groups[tld]) row_names.append(tld + '-TOTAL') row_names.append(TIER_TOTAL) #unit_extractor = WLCG_UNIT_MAPPING.get(unit, WLCG_UNIT_MAPPING_DEFAULT) unit_extractor = lambda rec : rec[unit] elements = [] for row in row_names: for col in columns: for rec in wlcg_records: if rec[wlcg.MACHINE_NAME] == row and rec[wlcg.VO_NAME] == col: value = _formatValue( unit_extractor(rec) ) # hurrah for formatting if row == TIER_TOTAL and col == TOTAL: value = htmltable.StyledTableValue(value, bold=True, double_underlined=True) elif (row.endswith('-TOTAL') and col == TOTAL) or row == TIER_TOTAL: value = htmltable.StyledTableValue(value, bold=True, underlined=True) elif row.endswith('-TOTAL') or row == TIER_TOTAL or col == TOTAL: value = htmltable.StyledTableValue(value, bold=True) elements.append( ((col,row), value)) break else: elements.append( ((col,row), '') ) matrix = dict(elements) table_content = htmltable.createHTMLTable(matrix, columns, row_names, column_labels=COLUMN_NAMES) # render page start_date_option = request.args.get('startdate', [''])[0] end_date_option = request.args.get('enddate', [''])[0] title = 'WLCG oversight view' unit_options = [] for u in self.units: unit_options.append(( u, COLUMN_NAMES[u] )) unit_buttons = html.createRadioButtons('unit', unit_options, checked_value=unit) selector_form = dateform.createMonthSelectorForm(self.path, start_date_option, end_date_option, unit_buttons) quarters = dateform.generateFormQuarters() quarter_links = [] for q in quarters: year, quart = dateform.parseQuarter(q) sd, ed = dateform.quarterStartEndDates(year, quart) quarter_links.append(html.createLink('%s?startdate=%s&enddate=%s' % (self.path, sd, ed), q ) ) range_text = html.createParagraph('Date range: %s - %s (%s days)' % (start_date, end_date, days)) request.write( html.HTML_VIEWBASE_HEADER % {'title': title} ) request.write( html.createTitle(title) ) request.write( html.createParagraph('Quarters: \n ' + (' ' + html.NBSP).join(quarter_links) ) ) request.write( html.SECTION_BREAK ) request.write( html.createParagraph(selector_form) ) request.write( html.SECTION_BREAK ) request.write( html.createParagraph(range_text) ) request.write( table_content ) request.write( html.SECTION_BREAK ) request.write( html.createParagraph('Query time: %s' % round(t_query, 2)) ) request.write( html.createParagraph('Data process time: %s' % round(t_dataprocess, 2)) ) request.write( html.HTML_VIEWBASE_FOOTER ) request.finish() return server.NOT_DONE_YET