7 from concurrent.futures
import ThreadPoolExecutor
12 from conditions_db
import cli_download, ConditionsDB, encode_name
13 from softwaretrigger
import db_access
17 """Small helper class as the difflib does not understand dicts directly (as they are not hashable)"""
20 """Create a hash for the object out of the json string"""
21 return hash(json.dumps(self))
25 """Helper class to translate the user-specified database(s) into parameters for basf2"""
28 """Init the stored databases and exp/run from the specified command argument"""
37 split_argument = command_argument.split(
":")
38 if len(split_argument) == 2:
39 command_argument, exp_run = split_argument
41 if exp_run !=
"latest":
45 raise argparse.ArgumentTypeError(
46 f
"Do not understand the exp/run argument '{exp_run}'")
48 elif len(split_argument) != 1:
49 raise argparse.ArgumentTypeError(
50 f
"Do not understand the database argument '{command_argument}'")
53 self.
_database = command_argument.split(
",")
56 def normalize(database):
58 if os.path.exists(database):
59 if os.path.basename(database) !=
"database.txt":
60 database = os.path.join(database,
"database.txt")
68 Set the basf2 database chain according to the specified databases.
69 Before that, clean up and invalidate everything from th database.
71 The distinction between file databases and global databases is done
72 via the fact of a file/folder with this name exists or not.
74 from ROOT
import Belle2
77 basf2.reset_database(
False)
78 basf2.conditions.override_globaltags()
81 if os.path.exists(database):
82 basf2.conditions.prepend_testing_payloads(database)
84 basf2.conditions.prepend_globaltag(database)
86 db_access.set_event_number(evt_number=0, run_number=int(self.
_run),
91 Get all cuts stored in the database(s)
92 and sort them according to base_identifier, cut_identifier.
96 all_cuts = db_access.get_all_cuts()
97 all_cuts = sorted(all_cuts,
98 key=
lambda cut: (cut[
"Base Identifier"], cut[
"Cut Identifier"]))
99 all_cuts = list(map(HashableCut, all_cuts))
103 def diff_function(args):
105 Show the diff between two specified databases.
107 first_database_cuts = args.first_database.get_all_cuts()
108 second_database_cuts = args.second_database.get_all_cuts()
110 diff = difflib.SequenceMatcher(
111 a=list(map(str, first_database_cuts)), b=list(map(str, second_database_cuts)))
113 def print_cut(cut, prefix=" "):
115 print(
"\x1b[31m", end=
"")
117 print(
"\x1b[32m", end=
"")
119 print(
"\x1b[0m", end=
"")
121 def print_cuts(prefix, cuts):
125 for tag, i1, i2, j1, j2
in diff.get_opcodes():
127 if args.only_changes:
129 print_cuts(
" ", diff.b[j1:j2])
130 if tag
in [
"delete",
"replace"]:
131 print_cuts(
"-", diff.a[i1:i2])
132 if tag
in [
"insert",
"replace"]:
133 print_cuts(
"+", diff.b[j1:j2])
136 def add_cut_function(args):
138 Add a cut with the given parameters and also add it to the trigger menu.
140 args.database.set_database()
142 db_access.upload_cut_to_db(cut_string=args.cut_string, base_identifier=args.base_identifier,
143 cut_identifier=args.cut_identifier, prescale_factor=args.prescale_factor,
144 reject_cut=args.reject_cut.lower() ==
"true", iov=
None)
145 trigger_menu = db_access.download_trigger_menu_from_db(args.base_identifier,
146 do_set_event_number=
False)
147 cuts = list(trigger_menu.getCutIdentifiers())
149 if args.cut_identifier
not in cuts:
150 cuts.append(args.cut_identifier)
152 db_access.upload_trigger_menu_to_db(args.base_identifier, cuts,
153 accept_mode=trigger_menu.isAcceptMode(), iov=
None)
156 def remove_cut_function(args):
158 Remove a cut with the given name from the trigger menu.
160 args.database.set_database()
162 trigger_menu = db_access.download_trigger_menu_from_db(
163 args.base_identifier, do_set_event_number=
False)
164 cuts = trigger_menu.getCutIdentifiers()
166 cuts = [cut
for cut
in cuts
if cut != args.cut_identifier]
167 db_access.upload_trigger_menu_to_db(
168 args.base_identifier, cuts, accept_mode=trigger_menu.isAcceptMode(), iov=
None)
171 def print_function(args):
173 Print the cuts stored in the database(s).
175 cuts = args.database.get_all_cuts()
176 df = pd.DataFrame(cuts)
178 if args.format ==
"pandas":
179 pd.set_option(
"display.max_rows", 500)
180 pd.set_option(
"display.max_colwidth", 200)
181 pd.set_option(
'display.max_columns', 500)
182 pd.set_option(
'display.width', 1000)
184 elif args.format ==
"jira":
185 from tabulate
import tabulate
186 print(tabulate(df, tablefmt=
"jira", showindex=
False, headers=
"keys"))
187 elif args.format ==
"grid":
188 from tabulate
import tabulate
189 print(tabulate(df, tablefmt=
"grid", showindex=
False, headers=
"keys"))
190 elif args.format ==
"json":
192 print(json.dumps(df.to_dict(
"records"), indent=2))
193 elif args.format ==
"list":
194 for base_identifier, cuts
in df.groupby(
"Base Identifier"):
195 for _, cut
in cuts.iterrows():
196 print(cut[
"Base Identifier"], cut[
"Cut Identifier"])
197 elif args.format ==
"human-readable":
198 print(
"Currently, the following menus and triggers are in the database")
199 for base_identifier, cuts
in df.groupby(
"Base Identifier"):
200 print(base_identifier)
202 print(
"\tUsed triggers:\n\t\t" +
203 ", ".join(list(cuts[
"Cut Identifier"])))
204 print(
"\tIs in accept mode:\n\t\t" +
205 str(cuts[
"Reject Menu"].iloc[0]))
206 for _, cut
in cuts.iterrows():
207 print(
"\t\tCut Name:\n\t\t\t" + cut[
"Cut Identifier"])
208 print(
"\t\tCut condition:\n\t\t\t" + cut[
"Cut Condition"])
209 print(
"\t\tCut prescaling\n\t\t\t" +
210 str(cut[
"Cut Prescaling"]))
211 print(
"\t\tCut is a reject cut:\n\t\t\t" +
212 str(cut[
"Reject Cut"]))
215 raise AttributeError(f
"Do not understand format {args.format}")
218 def create_script_function(args):
220 Print the b2hlt_trigger commands to create a lobal database copy.
222 cuts = args.database.get_all_cuts()
223 df = pd.DataFrame(cuts)
225 sfmt =
'b2hlt_triggers add_cut \
226 "{Base Identifier}" "{Cut Identifier}" "{Cut Condition}" "{Cut Prescaling}" "{Reject Cut}"'.format
227 if args.filename
is None:
228 df.apply(
lambda x: print(sfmt(**x)), 1)
230 with open(args.filename,
'w')
as f:
231 df.apply(
lambda x: f.write(sfmt(**x) +
'\n'), 1)
234 def iov_includes(iov_list, exp, run):
236 Comparison function between two IoVs (start, end) stored in the database and
237 the given exp/run combination.
240 copied_iov_list = iov_list[2:]
241 copied_iov_list = list(map(
lambda x: x
if x != -1
else float(
"inf"), copied_iov_list))
243 exp_start, run_start, exp_end, run_end = copied_iov_list
245 return (exp_start, run_start) <= (exp, run) <= (exp_end, run_end)
248 def download_function(args):
250 Download the trigger cuts in the given database to disk and set their IoV to infinity.
252 if len(args.database._database) != 1:
253 raise AttributeError(
"Can only download from a single database! Please do not specify more than one.")
255 global_tag = args.database._database[0]
258 os.makedirs(args.destination, exist_ok=
True)
261 req = db.request(
"GET", f
"/globalTag/{encode_name(global_tag)}/globalTagPayloads",
262 f
"Downloading list of payloads for {global_tag} tag")
265 for payload
in req.json():
266 name = payload[
"payloadId"][
"basf2Module"][
"name"]
267 if not name.startswith(
"software_trigger_cut"):
270 local_file, remote_file, checksum, iovlist = cli_download.check_payload(args.destination, payload)
272 new_iovlist = list(
filter(
lambda iov: iov_includes(iov, args.database._experiment, args.database._run), iovlist))
276 if local_file
in download_list:
277 download_list[local_file][-1] += iovlist
279 download_list[local_file] = [local_file, remote_file, checksum, iovlist]
284 with ThreadPoolExecutor(max_workers=20)
as pool:
285 for iovlist
in pool.map(
lambda x: cli_download.download_file(db, *x), download_list.values()):
290 full_iovlist += iovlist
293 for iov
in sorted(full_iovlist):
295 iov = [iov[0], iov[1], 0, 0, -1, -1]
296 dbfile.append(
"dbstore/{} {} {},{},{},{}\n".format(*iov))
297 with open(os.path.join(args.destination,
"database.txt"),
"w")
as txtfile:
298 txtfile.writelines(dbfile)
303 Main function to be called from b2hlt_triggers.
305 parser = argparse.ArgumentParser(
307 Execute different actions on stored trigger menus in the database.
309 Call with `%(prog)s [command] --help` to get a description on each command.
310 Please also see the examples at the end of this help.
312 Many commands require one (or many) specified databases. Different formats are possible.
313 All arguments need to be written in quotation marks.
314 * "online" Use the latest version in the "online" database
315 (or any other specified global tag).
316 * "online:latest" Same as just "online", makes things a bit clearer.
317 * "online:8/345" Use the version in the "online" database (or any other specified global tag)
318 which was present in exp 8 run 345.
319 * "localdb:4/42" Use the local database specified in the given folder for the given exp/run.
320 * "localdb/database.txt" It is also possible to specify a file directly.
321 * "online,localdb" First look in localdb, then in the online GT
322 * "online,localdb:9/1" Can also be combined with the exp/run (It is then valid for all database accesses)
326 * Check what has changed between 8/1 and 9/1 in the online GT.
328 %(prog)s diff --first-database "online:8/1" --second-database "online:9/1" --only-changes
330 * Especially useful while editing trigger cuts and menus: check what has changed between the latest
331 version online and what is currently additionally in localdb
333 %(prog)s diff --first-database "online:latest" --second-database "online,localdb:latest"
335 This use case is so common, it is even the default
339 * Print the latest version of the cuts in online (plus what is defined in the localdb) in a human-friendly way
343 * Print the version of the cuts which was present in 8/1 online in a format understandable by JIRA
344 (you need to have the tabulate package installed)
346 %(prog)s print --database "online:8/1" --format jira
348 * Add a new skim cut named "accept_b2bcluster_3D" with the specified parameters and upload it to localdb
350 %(prog)s add_cut skim accept_b2bcluster_3D "[[nB2BCC3DLE >= 1] and [G1CMSBhabhaLE < 2]]" 1 False
352 * Remove the cut "accept_bhabha" from the trigger menu "skim"
354 %(prog)s remove_cut skim accept_bhabha
356 * Download the latest state of the triggers into the folder "localdb", e.g. to be used for local studies
361 formatter_class=argparse.RawDescriptionHelpFormatter,
362 usage=
"%(prog)s command"
364 parser.set_defaults(func=
lambda *args: parser.print_help())
365 subparsers = parser.add_subparsers(title=
"command",
366 description=
"Choose the command to execute")
369 diff_parser = subparsers.add_parser(
"diff", help=
"Compare the trigger menu in two different databases.",
370 formatter_class=argparse.RawDescriptionHelpFormatter,
372 Compare the two trigger menus present in the two specified databases
373 (or database chains) for the given exp/run combination (or the latest
375 Every line in the output is one trigger line. A "+" in front means the
376 trigger line is present in the second database, but not in the first.
377 A "-" means exactly the opposite. Updates trigger lines will show up
378 as both "-" and "+" (with different parameters).
380 The two databases (or database chains) can be specified as describes
381 in the general help (check b2hlt_triggers --help).
382 By default, the latest version of the online database will be
383 compared with what is defined on top in the localdb.
385 diff_parser.add_argument(
"--first-database", help=
"First database to compare. Defaults to 'online:latest'.",
387 diff_parser.add_argument(
"--second-database", help=
"Second database to compare. Defaults to 'online,localdb:latest'.",
389 diff_parser.add_argument(
390 "--only-changes", help=
"Do not show unchanged lines.", action=
"store_true")
391 diff_parser.set_defaults(func=diff_function)
394 print_parser = subparsers.add_parser(
"print", help=
"Print the cuts stored in the given database.",
395 formatter_class=argparse.RawDescriptionHelpFormatter,
397 Print the defined trigger menu and trigger cuts in a human-friendly
398 (default) or machine-friendly way.
399 The database (or database chain) needs to be specified in the general
400 help (check b2hlt_triggers --help).
402 For additional formatting options please install the tabulate package with
404 pip3 install --user tabulate
406 By default the latest version on the online database and what is defined on
407 top in the localdb will be shown.
409 print_parser.add_argument(
"--database", help=
"Which database to print. Defaults to 'online,localdb:latest'.",
411 choices = [
"human-readable",
"json",
"list",
"pandas"]
413 from tabulate
import tabulate
414 choices += [
'jira',
'grid']
417 print_parser.add_argument(
"--format", help=
"Choose the format how to print the trigger cuts. "
418 "To get access to more options please install the tabulate package using pip",
419 choices=choices, default=
"human-readable")
420 print_parser.set_defaults(func=print_function)
423 create_script_parser = subparsers.add_parser(
425 help=
"Create b2hlt_triggers command to create a online globaltag copy.",
426 formatter_class=argparse.RawDescriptionHelpFormatter,
428 Generate the required b2hlt_trigger commands to reproduce an online globaltag for a given exp/run
429 number to create a local database version of it.
431 create_script_parser.add_argument(
"--database", help=
"Which database to print. Defaults to 'online:latest'.",
433 create_script_parser.add_argument(
"--filename", default=
None,
434 help=
"Write to given filename instead of stdout.")
435 create_script_parser.set_defaults(func=create_script_function)
438 add_cut_parser = subparsers.add_parser(
"add_cut", help=
"Add a new cut.",
439 formatter_class=argparse.RawDescriptionHelpFormatter,
441 Add a cut with the given properties and upload it into the localdb database.
442 After that, you can upload it to the central database, to e.g. staging_online.
444 As a base line for editing, a database much be specified in the usual format
445 (check b2hlt_triggers --help).
446 It defaults to the latest version online and the already present changes in
448 Please note that the IoV of the created trigger line and menu is set to infinite.
450 add_cut_parser.add_argument(
"--database", help=
"Where to take the trigger menu from. Defaults to 'online,localdb:latest'.",
452 add_cut_parser.add_argument(
"base_identifier",
453 help=
"base_identifier of the cut to add", choices=[
"filter",
"skim"])
454 add_cut_parser.add_argument(
"cut_identifier",
455 help=
"cut_identifier of the cut to add")
456 add_cut_parser.add_argument(
"cut_string",
457 help=
"cut_string of the cut to add")
458 add_cut_parser.add_argument(
"prescale_factor", type=int,
459 help=
"prescale of the cut to add")
460 add_cut_parser.add_argument(
461 "reject_cut", help=
"Is the new cut a reject cut?")
462 add_cut_parser.set_defaults(func=add_cut_function)
465 remove_cut_parser = subparsers.add_parser(
"remove_cut", help=
"Remove a cut of the given name.",
466 formatter_class=argparse.RawDescriptionHelpFormatter,
468 Remove a cut with the given base and cut identifier from the trigger menu
469 and upload the new trigger menu to the localdb.
470 After that, you can upload it to the central database, to e.g. staging_online.
472 As a base line for editing, a database much be specified in the usual format
473 (check b2hlt_triggers --help).
474 It defaults to the latest version online and the already present changes in
476 Please note that the IoV of the created trigger menu is set to infinite.
478 The old cut payload will not be deleted from the database. This is not
479 needed as only cuts specified in a trigger menu are used.
481 remove_cut_parser.add_argument(
"base_identifier",
482 help=
"base_identifier of the cut to delete", choices=[
"filter",
"skim"])
483 remove_cut_parser.add_argument(
"cut_identifier",
484 help=
"cut_identifier of the cut to delete")
485 remove_cut_parser.add_argument(
"--database",
486 help=
"Where to take the trigger menu from. Defaults to 'online,localdb:latest'.",
488 remove_cut_parser.set_defaults(func=remove_cut_function)
491 download_parser = subparsers.add_parser(
"download", help=
"Download the trigger menu from the database.",
492 formatter_class=argparse.RawDescriptionHelpFormatter,
494 Download all software trigger related payloads from the specified database
495 into the folder localdb and create a localdb/database.txt. This is
496 especially useful when doing local trigger studies which should use the
497 latest version of the online triggers. By default, the latest
498 version of the online GT will be downloaded.
500 Attention: this script will override a database defined in the destination
501 folder (default localdb)!
502 Attention 2: all IoVs of the downloaded triggers will be set to 0, 0, -1, -1
503 so you can use the payloads fro your local studies for whatever run you want.
504 This should not (never!) be used to upload or edit new triggers and
505 is purely a convenience function to synchronize your local studies
506 with the online database!
508 Please note that for this command you can only specify a single database
509 (all others can work with multiple databases).
511 download_parser.add_argument(
"--database",
512 help=
"Single database where to take the trigger menu from. Defaults to 'online:latest'.",
514 download_parser.add_argument(
"--destination",
515 help=
"In which folder to store the output", default=
"localdb")
516 download_parser.set_defaults(func=download_function)
518 args = parser.parse_args()