Belle II Software prerelease-11-00-00a
cli.py
1#!/usr/bin/env python3
2
3
10import argparse
11import os
12import json
13from concurrent.futures import ThreadPoolExecutor
14import difflib
15import pandas as pd
16
17import basf2
18from conditions_db import cli_download, ConditionsDB, encode_name
19from softwaretrigger import db_access
20
21
22class HashableCut(dict):
23 """Small helper class as the difflib does not understand dicts directly (as they are not hashable)"""
24
25 def __hash__(self):
26 """Create a hash for the object out of the json string"""
27 return hash(json.dumps(self))
28
29
31 """Helper class to translate the user-specified database(s) into parameters for basf2"""
32
33 def __init__(self, command_argument):
34 """Init the stored databases and exp/run from the specified command argument"""
35
36 self._database = []
37
38 self._experiment = 99999
39
40 self._run = 99999
41
42 # If given, use the experiment run from the command_argument
43 split_argument = command_argument.split(":")
44 if len(split_argument) == 2:
45 command_argument, exp_run = split_argument
46
47 if exp_run != "latest":
48 try:
49 self._experiment, self._run = map(int, exp_run.split("/"))
50 except BaseException:
51 raise argparse.ArgumentTypeError(
52 f"Do not understand the exp/run argument '{exp_run}'")
53
54 elif len(split_argument) != 1:
55 raise argparse.ArgumentTypeError(
56 f"Do not understand the database argument '{command_argument}'")
57
58 # Now split up the databases
59 self._database = command_argument.split(",")
60
61 # However make sure we have them in the correct format
62 def normalize(database):
63 # In case a local file is specified we can just use it directly
64 if os.path.exists(database):
65 if os.path.basename(database) != "database.txt":
66 database = os.path.join(database, "database.txt")
67
68 return database
69
70 self._database = list(map(normalize, self._database))
71
72 def set_database(self):
73 """
74 Set the basf2 database chain according to the specified databases.
75 Before that, clean up and invalidate everything from th database.
76
77 The distinction between file databases and global databases is done
78 via the fact of a file/folder with this name exists or not.
79 """
80 from ROOT import Belle2
82
83 basf2.conditions.override_globaltags()
84
85 for database in self._database:
86 if os.path.exists(database):
87 basf2.conditions.prepend_testing_payloads(database)
88 else:
89 basf2.conditions.prepend_globaltag(database)
90
91 db_access.set_event_number(evt_number=0, run_number=int(self._run),
92 exp_number=int(self._experiment))
93
94 def get_all_cuts(self):
95 """
96 Get all cuts stored in the database(s)
97 and sort them according to base_identifier, cut_identifier.
98 """
99 self.set_database()
100
101 all_cuts = db_access.get_all_cuts()
102 all_cuts = sorted(all_cuts,
103 key=lambda cut: (cut["Base Identifier"], cut["Cut Identifier"]))
104 all_cuts = list(map(HashableCut, all_cuts))
105 return all_cuts
106
107
108def diff_function(args):
109 """
110 Show the diff between two specified databases.
111 """
112 first_database_cuts = args.first_database.get_all_cuts()
113 second_database_cuts = args.second_database.get_all_cuts()
114
115 diff = difflib.SequenceMatcher(
116 a=list(map(str, first_database_cuts)), b=list(map(str, second_database_cuts)))
117
118 def print_cut(cut, prefix=" "):
119 if prefix == "-":
120 print("\x1b[31m", end="")
121 elif prefix == "+":
122 print("\x1b[32m", end="")
123 print(prefix, cut)
124 print("\x1b[0m", end="")
125
126 def print_cuts(prefix, cuts):
127 for c in cuts:
128 print_cut(c, prefix)
129
130 for tag, i1, i2, j1, j2 in diff.get_opcodes():
131 if tag == "equal":
132 if args.only_changes:
133 continue
134 print_cuts(" ", diff.b[j1:j2])
135 if tag in ["delete", "replace"]:
136 print_cuts("-", diff.a[i1:i2])
137 if tag in ["insert", "replace"]:
138 print_cuts("+", diff.b[j1:j2])
139
140
141def add_cut_function(args):
142 """
143 Add a cut with the given parameters and also add it to the trigger menu.
144 """
145 args.database.set_database()
146
147 db_access.upload_cut_to_db(cut_string=args.cut_string, base_identifier=args.base_identifier,
148 cut_identifier=args.cut_identifier, prescale_factor=args.prescale_factor,
149 reject_cut=args.reject_cut.lower() == "true", iov=None)
150 trigger_menu = db_access.download_trigger_menu_from_db(args.base_identifier,
151 do_set_event_number=False)
152 if trigger_menu is None:
153 print(f"Trigger menu '{args.base_identifier}' not found. Creating a new one.")
154
155 db_access.upload_trigger_menu_to_db(args.base_identifier, [args.cut_identifier], accept_mode=True, iov=None)
156
157 cuts = [str(cut) for cut in trigger_menu.getCutIdentifiers()]
158
159 if args.cut_identifier not in cuts:
160 cuts.append(args.cut_identifier)
161
162 db_access.upload_trigger_menu_to_db(args.base_identifier, cuts,
163 accept_mode=trigger_menu.isAcceptMode(), iov=None)
164
165
166def remove_cut_function(args):
167 """
168 Remove a cut with the given name from the trigger menu.
169 """
170 args.database.set_database()
171
172 trigger_menu = db_access.download_trigger_menu_from_db(
173 args.base_identifier, do_set_event_number=False)
174 cuts = [str(cut) for cut in trigger_menu.getCutIdentifiers() if str(cut) != args.cut_identifier]
175
176 db_access.upload_trigger_menu_to_db(
177 args.base_identifier, cuts, accept_mode=trigger_menu.isAcceptMode(), iov=None)
178
179
180def print_function(args):
181 """
182 Print the cuts stored in the database(s).
183 """
184 cuts = args.database.get_all_cuts()
185 df = pd.DataFrame(cuts)
186
187 if args.format == "pandas":
188 pd.set_option("display.max_rows", 500)
189 pd.set_option("display.max_colwidth", 200)
190 pd.set_option('display.max_columns', 500)
191 pd.set_option('display.width', 1000)
192 print(df)
193 elif args.format in ["github", "gitlab"]:
194 from tabulate import tabulate
195 print(tabulate(df, tablefmt="github", showindex=False, headers="keys"))
196 elif args.format == "grid":
197 from tabulate import tabulate
198 print(tabulate(df, tablefmt="grid", showindex=False, headers="keys"))
199 elif args.format == "json":
200 import json
201 print(json.dumps(df.to_dict("records"), indent=2))
202 elif args.format == "list":
203 for base_identifier, cuts in df.groupby("Base Identifier"):
204 for _, cut in cuts.iterrows():
205 print(cut["Base Identifier"], cut["Cut Identifier"])
206 elif args.format == "human-readable":
207 print("Currently, the following menus and triggers are in the database")
208 for base_identifier, cuts in df.groupby("Base Identifier"):
209 print(base_identifier)
210 print("")
211 print("\tUsed triggers:\n\t\t" +
212 ", ".join(list(cuts["Cut Identifier"])))
213 print("\tIs in accept mode:\n\t\t" +
214 str(cuts["Reject Menu"].iloc[0]))
215 for _, cut in cuts.iterrows():
216 print("\t\tCut Name:\n\t\t\t" + cut["Cut Identifier"])
217 print("\t\tCut condition:\n\t\t\t" + cut["Cut Condition"])
218 print("\t\tCut prescaling\n\t\t\t" +
219 str(cut["Cut Prescaling"]))
220 print("\t\tCut is a reject cut:\n\t\t\t" +
221 str(cut["Reject Cut"]))
222 print()
223 else:
224 raise AttributeError(f"Do not understand format {args.format}")
225
226
227def create_script_function(args):
228 """
229 Print the b2hlt_trigger commands to create a lobal database copy.
230 """
231 cuts = args.database.get_all_cuts()
232 df = pd.DataFrame(cuts)
233
234 sfmt = 'b2hlt_triggers add_cut \
235"{Base Identifier}" "{Cut Identifier}" "{Cut Condition}" "{Cut Prescaling}" "{Reject Cut}"'.format
236 if args.filename is None:
237 df.apply(lambda x: print(sfmt(**x)), 1)
238 else:
239 with open(args.filename, 'w') as f:
240 df.apply(lambda x: f.write(sfmt(**x) + '\n'), 1)
241
242
243def iov_includes(iov_list, exp, run):
244 """
245 Comparison function between two IoVs (start, end) stored in the database and
246 the given exp/run combination.
247 """
248 # Dirty hack: replace -1 by infinity to make the comparison easier
249 copied_iov_list = iov_list[2:]
250 copied_iov_list = list(map(lambda x: x if x != -1 else float("inf"), copied_iov_list))
251
252 exp_start, run_start, exp_end, run_end = copied_iov_list
253
254 return (exp_start, run_start) <= (exp, run) <= (exp_end, run_end)
255
256
257def download_function(args):
258 """
259 Download the trigger cuts in the given database to disk and set their IoV to infinity.
260 """
261 if len(args.database._database) != 1:
262 raise AttributeError("Can only download from a single database! Please do not specify more than one.")
263
264 global_tag = args.database._database[0]
265
266 # The following is an adapted version of cli_download
267 os.makedirs(args.destination, exist_ok=True)
268
269 db = ConditionsDB()
270 req = db.request("GET", f"/globalTag/{encode_name(global_tag)}/globalTagPayloads",
271 f"Downloading list of payloads for {global_tag} tag")
272
273 download_list = {}
274 for payload in req.json():
275 name = payload["payloadId"]["basf2Module"]["name"]
276 if not name.startswith("software_trigger_cut"):
277 continue
278
279 local_file, remote_file, checksum, iovlist = cli_download.check_payload(args.destination, payload)
280
281 new_iovlist = list(filter(lambda iov: iov_includes(iov, args.database._experiment, args.database._run), iovlist))
282 if not new_iovlist:
283 continue
284
285 if local_file in download_list:
286 download_list[local_file][-1] += iovlist
287 else:
288 download_list[local_file] = [local_file, remote_file, checksum, iovlist]
289
290 # do the downloading
291 full_iovlist = []
292 failed = 0
293 with ThreadPoolExecutor(max_workers=20) as pool:
294 for iovlist in pool.map(lambda x: cli_download.download_file(db, *x), download_list.values()):
295 if iovlist is None:
296 failed += 1
297 continue
298
299 full_iovlist += iovlist
300
301 dbfile = []
302 for iov in sorted(full_iovlist):
303 # Set the IoV intentionally to 0, 0, -1, -1
304 iov = [iov[0], iov[1], 0, 0, -1, -1]
305 dbfile.append("dbstore/{} {} {},{},{},{}\n".format(*iov))
306 with open(os.path.join(args.destination, "database.txt"), "w") as txtfile:
307 txtfile.writelines(dbfile)
308
309
310def main():
311 """
312 Main function to be called from b2hlt_triggers.
313 """
314 parser = argparse.ArgumentParser(
315 description="""
316Execute different actions on stored trigger menus in the database.
317
318Call with `%(prog)s [command] --help` to get a description on each command.
319Please also see the examples at the end of this help.
320
321Many commands require one (or many) specified databases. Different formats are possible.
322All arguments need to be written in quotation marks.
323* "online" Use the latest version in the "online" database
324 (or any other specified global tag).
325* "online:latest" Same as just "online", makes things a bit clearer.
326* "online:8/345" Use the version in the "online" database (or any other specified global tag)
327 which was present in exp 8 run 345.
328* "localdb:4/42" Use the local database specified in the given folder for the given exp/run.
329* "localdb/database.txt" It is also possible to specify a file directly.
330* "online,localdb" First look in localdb, then in the online GT
331* "online,localdb:9/1" Can also be combined with the exp/run (It is then valid for all database accesses)
332
333Examples:
334
335* Check what has changed between 8/1 and 9/1 in the online GT.
336
337 %(prog)s diff --first-database "online:8/1" --second-database "online:9/1" --only-changes
338
339* Especially useful while editing trigger cuts and menus: check what has changed between the latest
340 version online and what is currently additionally in localdb
341
342 %(prog)s diff --first-database "online:latest" --second-database "online,localdb:latest"
343
344 This use case is so common, it is even the default
345
346 %(prog)s diff
347
348* Print the latest version of the cuts in online (plus what is defined in the localdb) in a human-friendly way
349
350 %(prog)s print
351
352* Print the version of the cuts which was present in 8/1 online in a format understandable by GitLab
353 (you need to have the tabulate package installed)
354
355 %(prog)s print --database "online:8/1" --format human-readable
356
357* Add a new skim cut named "accept_b2bcluster_3D" with the specified parameters and upload it to localdb
358
359 %(prog)s add_cut skim accept_b2bcluster_3D "[[nB2BCC3DLE >= 1] and [G1CMSBhabhaLE < 2]]" 1 False
360
361* Remove the cut "accept_bhabha" from the trigger menu "skim"
362
363 %(prog)s remove_cut skim accept_bhabha
364
365* Download the latest state of the triggers into the folder "localdb", e.g. to be used for local studies
366
367 %(prog)s download
368
369 """,
370 formatter_class=argparse.RawDescriptionHelpFormatter,
371 usage="%(prog)s command"
372 )
373 parser.set_defaults(func=lambda *args: parser.print_help())
374 subparsers = parser.add_subparsers(title="command",
375 description="Choose the command to execute")
376
377 # diff command
378 diff_parser = subparsers.add_parser("diff", help="Compare the trigger menu in two different databases.",
379 formatter_class=argparse.RawDescriptionHelpFormatter,
380 description="""
381Compare the two trigger menus present in the two specified databases
382(or database chains) for the given exp/run combination (or the latest
383version).
384Every line in the output is one trigger line. A "+" in front means the
385trigger line is present in the second database, but not in the first.
386A "-" means exactly the opposite. Updates trigger lines will show up
387as both "-" and "+" (with different parameters).
388
389The two databases (or database chains) can be specified as describes
390in the general help (check b2hlt_triggers --help).
391By default, the latest version of the online database will be
392compared with what is defined on top in the localdb.
393 """)
394 diff_parser.add_argument("--first-database", help="First database to compare. Defaults to 'online:latest'.",
395 type=DownloadableDatabase, default=DownloadableDatabase("online:latest"))
396 diff_parser.add_argument("--second-database", help="Second database to compare. Defaults to 'online,localdb:latest'.",
397 type=DownloadableDatabase, default=DownloadableDatabase("online,localdb:latest"))
398 diff_parser.add_argument(
399 "--only-changes", help="Do not show unchanged lines.", action="store_true")
400 diff_parser.set_defaults(func=diff_function)
401
402 # print command
403 print_parser = subparsers.add_parser("print", help="Print the cuts stored in the given database.",
404 formatter_class=argparse.RawDescriptionHelpFormatter,
405 description="""
406Print the defined trigger menu and trigger cuts in a human-friendly
407(default) or machine-friendly way.
408The database (or database chain) needs to be specified in the general
409help (check b2hlt_triggers --help).
410
411For additional formatting options please install the tabulate package with
412
413 pip3 install --user tabulate
414
415By default the latest version on the online database and what is defined on
416top in the localdb will be shown.
417 """)
418 print_parser.add_argument("--database", help="Which database to print. Defaults to 'online,localdb:latest'.",
419 type=DownloadableDatabase, default=DownloadableDatabase("online,localdb:latest"))
420 choices = ["human-readable", "json", "list", "pandas"]
421 try:
422 from tabulate import tabulate # noqa
423 choices += ['github', 'gitlab', 'grid']
424 except ImportError:
425 pass
426 print_parser.add_argument("--format", help="Choose the format how to print the trigger cuts. "
427 "To get access to more options please install the tabulate package using pip",
428 choices=choices, default="human-readable")
429 print_parser.set_defaults(func=print_function)
430
431 # create-script command
432 create_script_parser = subparsers.add_parser(
433 "create_script",
434 help="Create b2hlt_triggers command to create a online globaltag copy.",
435 formatter_class=argparse.RawDescriptionHelpFormatter,
436 description="""
437Generate the required b2hlt_trigger commands to reproduce an online globaltag for a given exp/run
438number to create a local database version of it.
439 """)
440 create_script_parser.add_argument("--database", help="Which database to print. Defaults to 'online:latest'.",
441 type=DownloadableDatabase, default=DownloadableDatabase("online:latest"))
442 create_script_parser.add_argument("--filename", default=None,
443 help="Write to given filename instead of stdout.")
444 create_script_parser.set_defaults(func=create_script_function)
445
446 # add_cut command
447 add_cut_parser = subparsers.add_parser("add_cut", help="Add a new cut.",
448 formatter_class=argparse.RawDescriptionHelpFormatter,
449 description="""
450Add a cut with the given properties and upload it into the localdb database.
451After that, you can upload it to the central database, to e.g. staging_online.
452
453As a base line for editing, a database much be specified in the usual format
454(check b2hlt_triggers --help).
455It defaults to the latest version online and the already present changes in
456localdb.
457Please note that the IoV of the created trigger line and menu is set to infinite.
458 """)
459 add_cut_parser.add_argument("--database", help="Where to take the trigger menu from. Defaults to 'online,localdb:latest'.",
460 type=DownloadableDatabase, default=DownloadableDatabase("online,localdb:latest"))
461 add_cut_parser.add_argument("base_identifier",
462 help="base_identifier of the cut to add", choices=["prefilter", "filter", "skim"])
463 add_cut_parser.add_argument("cut_identifier",
464 help="cut_identifier of the cut to add")
465 add_cut_parser.add_argument("cut_string",
466 help="cut_string of the cut to add")
467 add_cut_parser.add_argument("prescale_factor", type=int,
468 help="prescale of the cut to add")
469 add_cut_parser.add_argument(
470 "reject_cut", help="Is the new cut a reject cut?")
471 add_cut_parser.set_defaults(func=add_cut_function)
472
473 # remove_cut command
474 remove_cut_parser = subparsers.add_parser("remove_cut", help="Remove a cut of the given name.",
475 formatter_class=argparse.RawDescriptionHelpFormatter,
476 description="""
477Remove a cut with the given base and cut identifier from the trigger menu
478and upload the new trigger menu to the localdb.
479After that, you can upload it to the central database, to e.g. staging_online.
480
481As a base line for editing, a database much be specified in the usual format
482(check b2hlt_triggers --help).
483It defaults to the latest version online and the already present changes in
484localdb.
485Please note that the IoV of the created trigger menu is set to infinite.
486
487The old cut payload will not be deleted from the database. This is not
488needed as only cuts specified in a trigger menu are used.
489 """)
490 remove_cut_parser.add_argument("base_identifier",
491 help="base_identifier of the cut to delete", choices=["prefilter", "filter", "skim"])
492 remove_cut_parser.add_argument("cut_identifier",
493 help="cut_identifier of the cut to delete")
494 remove_cut_parser.add_argument("--database",
495 help="Where to take the trigger menu from. Defaults to 'online,localdb:latest'.",
496 type=DownloadableDatabase, default=DownloadableDatabase("online,localdb:latest"))
497 remove_cut_parser.set_defaults(func=remove_cut_function)
498
499 # download command
500 download_parser = subparsers.add_parser("download", help="Download the trigger menu from the database.",
501 formatter_class=argparse.RawDescriptionHelpFormatter,
502 description="""
503Download all software trigger related payloads from the specified database
504into the folder localdb and create a localdb/database.txt. This is
505especially useful when doing local trigger studies which should use the
506latest version of the online triggers. By default, the latest
507version of the online GT will be downloaded.
508
509Attention: this script will override a database defined in the destination
510folder (default localdb)!
511Attention 2: all IoVs of the downloaded triggers will be set to 0, 0, -1, -1
512so you can use the payloads from your local studies for whatever run you want.
513This should not (never!) be used to upload or edit new triggers and
514is purely a convenience function to synchronize your local studies
515with the online database!
516
517Please note that for this command you can only specify a single database
518(all others can work with multiple databases).
519 """)
520 download_parser.add_argument("--database",
521 help="Single database where to take the trigger menu from. Defaults to 'online:latest'.",
522 type=DownloadableDatabase, default=DownloadableDatabase("online:latest"))
523 download_parser.add_argument("--destination",
524 help="In which folder to store the output", default="localdb")
525 download_parser.set_defaults(func=download_function)
526
527 args = parser.parse_args()
528 args.func(args)
STL class.
int _experiment
the experiment number, default (= latest) is 99999
Definition cli.py:38
__init__(self, command_argument)
Definition cli.py:33
int _run
the run number, default (= latest) is 99999
Definition cli.py:40
list _database
the specified databases
Definition cli.py:36
static DBStore & Instance()
Instance of a singleton DBStore.
Definition DBStore.cc:26
Definition main.py:1