9from typing
import Dict, Any, List, Tuple
15from multiprocessing
import Process, Queue
33from validationplots
import create_plots
34import validationfunctions
37g_plottingProcesses: Dict[str, Tuple[Process, Queue, Dict[str, Any]]] = ({})
40def get_revision_label_from_json_filename(json_filename: str) -> str:
42 Gets the label of a revision from the path to the revision.json file
43 for example results/r121/revision.json
44 will result
in the label r121
45 This
is useful
if the results folder has been moved by the user
47 folder_part = os.path.split(json_filename)[0]
48 last_folder = os.path.basename(folder_part)
53def get_json_object_list(
54 results_folder: str, json_file_name: str
57 Searches one folder's sub-folder for json files of a specific name and returns a combined list of the
61 search_string = results_folder + "/*/" + json_file_name
63 found_revs = glob(search_string)
66 for r_file
in found_revs:
68 with open(r_file)
as json_file:
69 data = json.load(json_file)
72 found_rev_labels.append(
73 get_revision_label_from_json_filename(r_file)
76 return found_rev_labels
79def deliver_json(file_name: str):
81 Simply load & parse a json file and return the
85 with open(file_name)
as json_file:
86 data = json.load(json_file)
90def create_revision_key(revision_names: List[str]) -> str:
92 Create a string key out of a revision list, which is handed to tho browser
93 in form of a progress key
95 return functools.reduce(
lambda x, y: x +
"-" + y, revision_names,
"")
98def check_plotting_status(progress_key: str):
100 Check the plotting status via the supplied progress_key
103 if progress_key
not in g_plottingProcesses:
106 process, qu, last_status = g_plottingProcesses[progress_key]
111 while not qu.empty():
112 msg = qu.get_nowait()
116 g_plottingProcesses[progress_key] = (process, qu, last_status)
124def warn_wrong_directory():
125 if not os.getcwd().endswith(
"html"):
127 f
"ERROR: Expected to be in HTML directory, but my current "
128 f
"working directory is {os.getcwd()}; abspath: {os.getcwd()}."
133def start_plotting_request(
134 revision_names: List[str], results_folder: str
137 Start a new comparison between the supplied revisions
143 rev_key = create_revision_key(revision_names)
146 if rev_key
in g_plottingProcesses:
147 logging.info(f
"Plotting request for {rev_key} still running")
164 os.path.dirname(results_folder),
168 g_plottingProcesses[rev_key] = (p, qu,
None)
170 logging.info(f
"Started process for plotting request {rev_key}")
178Under here are functions that enable the validationserver to
179interact directly with the Gitlab project page to track and
183Config file with project information and access token in the local
184machine. This is expected to be in the validation/config folder, in
185the same root directory as the html_static files.
187A check is performed to see if the config file exists with all the
188relevant details and all of Gitlab functionalities are enabled/disabled
191When the server is being set up, a Gitlab object is created, which
192will be used subsequently to make all the API calls.
194As a final server initialization step, the project is queried to
195check if any of the current results are linked to existing issues
196and the result files are updated accordingly.
198The create/update issue functionality is accessible from the plot
199container. All the relevant pages are part of the
200validationserver cherry object.
202Issues created by the validation server will contain a block of
203automated code at the end of description and can be easily
204filtered from the GitLab issues page using the search string
205"Automated code, please do not delete". Relevant plots will be
206listed as a note in the issue page, along with the revision label.
208Function to upload files to Gitlab helps with pushing error plots
213def get_gitlab_config(
215) -> configparser.ConfigParser:
217 Parse the configuration file to be used to authenticate
218 GitLab API and retrieve relevant project info.
221 gitlab configparser object
224 gitlab_config = configparser.ConfigParser()
225 gitlab_config.read(config_path)
230def create_gitlab_object(config_path: str) -> gitlab.Gitlab:
232 Establish connection with Gitlab using a private access key
and return
233 a Gitlab object that can be used to make API calls. Default config
234 from the passed ini file will be used.
240 gitlab_object = gitlab.Gitlab.from_config(
241 config_files=[config_path]
245 logging.info(
"Established connection with Gitlab")
246 except gitlab.exceptions.GitlabAuthenticationError:
249 "Issue with authenticating GitLab. "
250 "Please ensure access token is correct and valid. "
251 "GitLab Integration will be disabled."
253 except requests.exceptions.Timeout:
256 "GitLab servers feeling under the weather, DESY outage? "
257 "GitLab Integration will be disabled."
263def get_project_object(
264 gitlab_object: gitlab.Gitlab, project_id: str
265) ->
'gitlab.project':
267 Fetch Gitlab project associated with the project ID.
270 gitlab project object
273 project = gitlab_object.projects.get(project_id, lazy=True)
278def search_project_issues(
279 gitlab_object: gitlab.Gitlab,
281 state: str =
'opened',
282) ->
'list[gitlab.issues]':
284 Search in the Gitlab
for open issues that contain the
288 gitlab project issues
291 issues = gitlab_object.issues.list(
302def update_linked_issues(
303 gitlab_object: gitlab.Gitlab, cwd_folder: str
306 Fetch linked issues and update the comparison json files.
313 search_key =
"Automated code, please do not delete"
314 issues = search_project_issues(gitlab_object, search_key)
315 past_issues = search_project_issues(gitlab_object, search_key,
'closed')
319 plot_issues = collections.defaultdict(list)
320 script_issues = collections.defaultdict(list)
321 pattern =
r"Relevant ([a-z]+): (\w+.*\w*)"
322 for i, issue
in enumerate(issues+past_issues):
323 match = re.search(pattern, issue.description)
325 if match.groups()[0] ==
'plot':
327 plot_issues[match.groups()[1]].append(-issue.iid)
329 plot_issues[match.groups()[1]].append(issue.iid)
331 script_issues[match.groups()[1]].append(issue.iid)
334 rev_list = get_json_object_list(
336 validationpath.file_name_comparison_json,
340 comparison_json_path = os.path.join(
343 validationpath.file_name_comparison_json,
345 comparison_json = deliver_json(comparison_json_path)
346 for package
in comparison_json[
"packages"]:
347 for plotfile
in package.get(
"plotfiles"):
348 for plot
in plotfile.get(
"plots"):
349 if plot[
"png_filename"]
in plot_issues.keys():
350 plot[
"issue"] = plot_issues[plot[
"png_filename"]]
354 with open(comparison_json_path,
"w")
as jsonFile:
355 json.dump(comparison_json, jsonFile, indent=4)
358 rev_list = get_json_object_list(
360 validationpath.file_name_results_json,
363 revision_json_path = os.path.join(
366 validationpath.file_name_results_json,
368 revision_json = deliver_json(revision_json_path)
369 for package
in revision_json[
"packages"]:
370 for scriptfile
in package.get(
"scriptfiles"):
371 if scriptfile[
"name"]
in script_issues.keys():
372 scriptfile[
"issues"] = script_issues[scriptfile[
'name']]
374 scriptfile[
"issues"] = []
376 with open(revision_json_path,
"w")
as jsonFile:
377 json.dump(revision_json, jsonFile, indent=4)
380def upload_file_gitlab(
381 file_path: str, project:
'gitlab.project'
384 Upload the passed file to the Gitlab project.
387 uploaded gitlab project file object
390 uploaded_file = project.upload(
391 file_path.split("/")[-1], filepath=file_path
397def get_librarians(package: str) -> List[str]:
399 Function to get package librarian(s)' GitLab usernames. Temp solution
400 until the .librarians file directly provides Gitlab usernames.
403 list of librarians' Gitlab usernames
407 librarian_file = os.path.join(
413 with open(librarian_file,
'r')
as f:
414 librarians = f.readlines()
415 except FileNotFoundError:
417 f
"{librarian_file} couldn't be found. Corrupted package/librarian file?"
422 desy_map_path = os.path.join(
423 "/home/b2soft/gitlab",
426 spec = importlib.util.spec_from_file_location(
'account_map', desy_map_path)
427 desy_map = importlib.util.module_from_spec(spec)
429 spec.loader.exec_module(desy_map)
430 except FileNotFoundError:
432 f
"{desy_map_path} couldn't be found. Have you setup Gitlab Webhook?"
436 for librarian
in librarians:
437 usernames.append(desy_map.get_gitlab_account(librarian.rstrip()))
446 gitlab_object: gitlab.Gitlab
449 Parse string to find email id(s) and then match them
with their Gitlab ids
450 using the userid map.
453 Dictionary
with list of Gitlab IDs
and corresponding list of
457 email_regex = re.compile(r"([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*"
458 r"|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([-!#-'*+/-9="
459 r"?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]"
462 email_ids = re.finditer(email_regex, contact)
469 with open(map_file,
'r')
as f:
470 id_map = f.readlines()
471 except FileNotFoundError:
473 f
"{map_file} couldn't be found. Did you get the location right?"
477 for email
in email_ids:
480 (line
for line
in id_map
if email.group()
in line),
None
484 f
"No userid found for {email} in the map, could it be that "
485 "they are (sadly) no longer in the collaboration?"
488 username = match.split(
' ')[1].rstrip()
489 assignees[
'usernames'].append(username)
492 f
"Map info {match} does not match the required format for map "
493 "'email gitlab_username'."
498 if not assignees[
'usernames']:
499 assignees[
'usernames'] = get_librarians(package)
501 "Couldn't find contact/id so assigning issue to the"
502 " package librarians."
505 for user
in assignees[
'usernames']:
507 assignees[
'gitlab_ids'].append(
508 gitlab_object.users.list(username=user)[0].id
512 f
"Could not find {user} in Gitlab."
516 "Issue will be assigned to "
517 f
"{[gitlab_object.users.get(id) for id in assignees['gitlab_ids']]}."
525def create_gitlab_issue(
528 uploaded_file: Dict[str, str],
529 assignees: Dict[str, List],
531 project:
'gitlab.project'
534 Create a new project issue with the passed title, description
and package,
541 issue = project.issues.create({"title": title,
542 "description": description,
543 "labels": [package,
'validation_issue']})
545 issue_note = issue.notes.create(
546 {
"body": f
'View the [error plot/log file]({uploaded_file["url"]}).'}
549 issue.assignee_ids = assignees[
'gitlab_ids']
552 if len(assignees[
'gitlab_ids']) > 1:
553 related_users = [f
'@{user} ' for user
in assignees[
'usernames'][1:]]
554 issue_note.body += f
"\n\nPinging {' '.join(related_users)}"
559 logging.info(f
"Created a new Gitlab issue - {issue.iid}")
564def update_gitlab_issue(
566 uploaded_file: Dict[str, str],
567 project:
'gitlab.project',
572 Update an existing project issue with the passed plotfile.
578 issue = project.issues.get(issue_iid)
579 name = file_path.split("/")[-1].split(
".")[0]
580 package = file_path.split(
"/")[-2]
583 if 'log' == file_path.split(
"/")[-1].split(
".")[-1]:
584 issue_type =
'script'
587 "body": f
'Related observation in validation of `{package}` package, `{name}`' +
588 f
'{issue_type} in `{rev_label}` build. View the [error plot/log file]({uploaded_file["url"]}).'
594 logging.info(f
"Updated existing Gitlab issue {issue.iid}")
597def update_scriptfile_issues_json(
598 revision_json_path: str,
599 scritptfile_name: str,
600 scritptfile_package: str,
604 Update the scriptfile's linked issues key in the relevant revision's
611 revision_json = deliver_json(revision_json_path)
612 for package
in revision_json[
"packages"]:
613 if package[
"name"] == scritptfile_package:
614 for scriptfile
in package.get(
"scriptfiles"):
615 if (scriptfile[
"name"] == scritptfile_name):
616 scriptfile[
"issues"].append(issue_id)
619 with open(revision_json_path,
"w")
as jsonFile:
620 json.dump(revision_json, jsonFile, indent=4)
623def update_plot_issues_json(
624 comparison_json_path: str,
630 Update the plotfile's linked issues key in the relevant comparison
637 comparison_json = deliver_json(comparison_json_path)
638 for package
in comparison_json[
"packages"]:
639 if package[
"name"] == plot_package:
640 for plotfile
in package.get(
"plotfiles"):
641 for plot
in plotfile.get(
"plots"):
642 if (plot[
"png_filename"] == plot_name):
643 plot[
"issue"].append(issue_id)
646 with open(comparison_json_path,
"w")
as jsonFile:
647 json.dump(comparison_json, jsonFile, indent=4)
653 Root Validation class to handle non-static HTTP requests into the
654 validation server. The two main functions are to hand out compiled json
655 objects of revisions
and comparisons
and to start
and monitor the
656 creation of comparison plots.
660 def __init__(self, working_folder, gitlab_object, gitlab_config, gitlab_map):
662 class initializer, which takes the path to the folders containing the
663 validation run results
and plots (aka comparison), gitlab object
and
675 os.environ["BELLE2_LOCAL_DIR"]
695 @cherrypy.tools.json_in()
696 @cherrypy.tools.json_out()
699 Triggers the start of a now comparison between the revisions supplied
702 rev_list = cherrypy.request.json["revision_list"]
703 logging.debug(
"Creating plots for revisions: " + str(rev_list))
704 progress_key = start_plotting_request(
708 return {
"progress_key": progress_key}
713 forward to the static landing page if
714 the default url
is used (like http://localhost:8080/)
716 raise cherrypy.HTTPRedirect(
"/static/validation.html")
719 def plots(self, *args):
721 Serve file from the html/plot directory.
722 :param args: For the request /plots/a/b/c, these will be the strings
726 warn_wrong_directory()
729 raise cherrypy.HTTPError(404)
731 tag_folder = os.path.relpath(
737 path = os.path.join(tag_folder, *args[-2:])
738 return cherrypy.lib.static.serve_file(path)
741 @cherrypy.tools.json_in()
742 @cherrypy.tools.json_out()
745 Checks on the status of a comparison creation
747 progress_key = cherrypy.request.json["input"]
748 logging.debug(
"Checking status for plot creation: " + str(progress_key))
749 status = check_plotting_status(progress_key)
753 @cherrypy.tools.json_out()
754 def revisions(self, revision_label=None):
756 Return a combined json object with all revisions
and
757 mark the newest one
with the field most_recent=true
761 rev_list = get_json_object_list(
763 validationpath.file_name_results_json,
768 reference_revision = json.loads(
774 full_path = os.path.join(
777 validationpath.file_name_results_json,
781 lbl_folder = get_revision_label_from_json_filename(full_path)
782 j = deliver_json(full_path)
783 j[
"label"] = lbl_folder
784 combined_list.append(j)
793 def sort_key(label: str):
796 f
"Misformatted label encountered: '{label}' "
797 f
"(doesn't seem to include date?)"
800 category, datetag = label.split(
"-", maxsplit=1)
801 print(category, datetag)
805 order = [
"release",
"prerelease",
"nightly"]
807 index = order.index(category)
811 f
"Misformatted label encountered: '{label}' (doesn't seem "
812 f
"to belong to any known category?)"
814 return f
"{index}-{datetag}"
816 combined_list.sort(key=
lambda rev: sort_key(rev[
"label"]), reverse=
True)
819 combined_list = [reference_revision] + combined_list
824 for r
in combined_list:
825 rdate_str = r[
"creation_date"]
826 if isinstance(rdate_str, str):
827 if len(rdate_str) > 0:
829 rdate = time.strptime(rdate_str,
"%Y-%m-%d %H:%M")
838 if newest_date
is None:
841 if rdate > newest_date:
845 for c
in combined_list:
846 if c[
"most_recent"]
is not None:
847 c[
"most_recent"] =
False
851 newest_rev[
"most_recent"] =
True
854 return {
"revisions": combined_list}
857 @cherrypy.tools.json_out()
860 return the json file of the comparison results of one specific
864 warn_wrong_directory()
878 if not os.path.isfile(path):
879 raise cherrypy.HTTPError(
880 404, f
"Json Comparison file {path} does not exist"
883 return deliver_json(path)
886 @cherrypy.tools.json_out()
890 JSON file containing git versions and time of last restart
893 warn_wrong_directory()
898 "last_restart": self.
last_restart.strftime(
"%-d %b %H:%M ")
900 "version_restart": self.
version,
902 os.environ[
"BELLE2_LOCAL_DIR"]
910 Metadata(str) of the file
912 cherrypy.response.headers['Content-Type'] =
'text/plain'
917 @cherrypy.tools.json_in()
918 @cherrypy.tools.json_out()
921 Call the functions to create the issue and redirect
922 to the created Gitlab issue page.
927 if 'log' == self.
file_path.split(
"/")[-1].split(
".")[-1]:
928 issue_type =
'script'
930 project_id = self.
gitlab_config[default_section][
'project_id']
933 uploaded_file = upload_file_gitlab(self.
file_path, project)
938 file_name = self.
file_path.split(
"/")[-1].split(
".log")[0]
939 file_package = self.
file_path.split(
"/")[-2]
941 assignees = parse_contact(
944 description +=
"\n\n---\n\n:robot: Automated code, please do not delete\n\n" + \
945 f
"Relevant {issue_type}: {file_name}\n\n" + \
946 f
"Revision label: {self.revision_label}\n\n---"
947 issue_id = create_gitlab_issue(
948 title, description, uploaded_file, assignees, file_package, project
955 if issue_type ==
'script':
956 revision_json_path = os.path.join(
959 validationpath.file_name_results_json,
961 update_scriptfile_issues_json(
962 revision_json_path, file_name, file_package, issue_id)
964 comparison_json_path = os.path.join(
969 update_plot_issues_json(
970 comparison_json_path, file_name, file_package, issue_id)
972 issue_url = self.
gitlab_config[default_section][
'project_url'] \
975 raise cherrypy.HTTPRedirect(
980 def issue(self, file_path, rev_label, contact):
982 Return a template issue creation interface
983 for the user to add title
and description.
993 return "ERROR: Gitlab integration not set up, verify config file."
995 raise cherrypy.HTTPRedirect(
"/static/validation_issue.html")
1000 Redirect to the Gitlab issue page.
1004 return "ERROR: Gitlab integration not set up, verify config file."
1006 issue_url = self.
gitlab_config[default_section][
'project_url'] \
1009 raise cherrypy.HTTPRedirect(
1016 Update existing issue in Gitlab
with current result plot
1017 and redirect to the updated Gitlab issue page.
1021 return "ERROR: Gitlab integration not set up, verify config file."
1023 plot_path = os.path.join(
1028 project_id = self.
gitlab_config[default_section][
'project_id']
1029 project = get_project_object(self.
gitlab_object, project_id)
1030 uploaded_file = upload_file_gitlab(plot_path, project)
1031 update_gitlab_issue(
1032 id, uploaded_file, project, plot_path, rev_label
1036 issue_url = self.
gitlab_config[default_section][
'project_url'] \
1040 raise cherrypy.HTTPRedirect(
1045def setup_gzip_compression(path, cherry_config):
1047 enable GZip compression for all text-based content the
1048 web-server will deliver
1051 cherry_config[path].update(
1053 "tools.gzip.on":
True,
1054 "tools.gzip.mime_types": [
1058 "application/javascript",
1065def get_argument_parser():
1067 Prepare a parser for all the known command line arguments
1071 parser = argparse.ArgumentParser()
1074 parser.add_argument(
1077 help=
"The IP address on which the"
1078 "server starts. Default is '127.0.0.1'.",
1080 default=
"127.0.0.1",
1082 parser.add_argument(
1085 help=
"The port number on which"
1086 " the server starts. Default is '8000'.",
1090 parser.add_argument(
1093 help=
"Open validation website" " in the system's default browser.",
1094 action=
"store_true",
1096 parser.add_argument(
1098 help=
"Run in production environment: "
1099 "no log/error output via website and no auto-reload",
1100 action=
"store_true",
1102 parser.add_argument(
1105 help=
"Path of file containing <email gitlab_username> map.",
1113def parse_cmd_line_arguments():
1115 Sets up a parser for command line arguments,
1116 parses them
and returns the arguments.
1117 @return: An object containing the parsed command line arguments.
1118 Arguments are accessed like they are attributes of the object,
1119 i.e. [name_of_object].[desired_argument]
1121 parser = get_argument_parser()
1123 return parser.parse_args()
1129 parse_command_line=False,
1135 logging.basicConfig(
1136 level=logging.DEBUG,
1137 format=
"%(asctime)s %(levelname)-8s %(message)s",
1142 cwd_folder = os.getcwd()
1146 os.environ.get(
"BELLE2_RELEASE_DIR",
None)
is None
1147 and os.environ.get(
"BELLE2_LOCAL_DIR",
None)
is None
1149 sys.exit(
"Error: No basf2 release set up!")
1151 cherry_config = dict()
1153 cherry_config[
"/"] = {}
1155 setup_gzip_compression(
"/", cherry_config)
1158 static_folder_list = [
"validation",
"html_static"]
1159 static_folder =
None
1161 if basepath[
"central"]
is not None:
1162 static_folder_central = os.path.join(
1163 basepath[
"central"], *static_folder_list
1165 if os.path.isdir(static_folder_central):
1166 static_folder = static_folder_central
1170 if basepath[
"local"]
is not None:
1171 static_folder_local = os.path.join(
1172 basepath[
"local"], *static_folder_list
1174 if os.path.isdir(static_folder_local):
1175 static_folder = static_folder_local
1177 if static_folder
is None:
1179 "Either BELLE2_RELEASE_DIR or BELLE2_LOCAL_DIR has to set "
1180 "to provide static HTML content. Did you run b2setup ?"
1187 logging.info(f
"Serving static content from {static_folder}")
1188 logging.info(f
"Serving result content and plots from {cwd_folder}")
1191 if not os.path.isdir(results_folder):
1193 f
"Result folder {results_folder} does not exist, run validate_basf2 first " +
1194 "to create validation output"
1197 results_count = sum(
1199 os.path.isdir(os.path.join(results_folder, f))
1200 for f
in os.listdir(results_folder)
1203 if results_count == 0:
1205 f
"Result folder {results_folder} contains no folders, run "
1206 f
"validate_basf2 first to create validation output"
1210 if not os.path.exists(
"html"):
1214 if not os.path.exists(
"plots"):
1217 if os.path.exists(
"plots/rainbow.json"):
1218 logging.info(
"Removing old plots and unpopular combinations")
1225 cherry_config[
"/static"] = {
1226 "tools.staticdir.on":
True,
1228 "tools.staticdir.match":
r"^.*\.(js|css|html|png|js.map)$",
1229 "tools.staticdir.dir": static_folder,
1231 setup_gzip_compression(
"/static", cherry_config)
1234 cherry_config[
"/plots"] = {
1235 "tools.staticdir.on":
True,
1237 "tools.staticdir.match":
r"^.*\.(png|json|pdf)$",
1238 "tools.staticdir.dir": comparison_folder,
1240 setup_gzip_compression(
"/plots", cherry_config)
1243 cherry_config[
"/results"] = {
1244 "tools.staticdir.on":
True,
1245 "tools.staticdir.dir": results_folder,
1247 "tools.staticdir.match":
r"^.*\.(log|root|txt)$",
1251 "tools.staticdir.content_types": {
1252 "log":
"text/plain; charset=utf-8",
1253 "root":
"application/octet-stream",
1257 setup_gzip_compression(
"/results", cherry_config)
1261 production_env =
False
1262 if parse_command_line:
1264 cmd_arguments = parse_cmd_line_arguments()
1266 ip = cmd_arguments.ip
1267 port = int(cmd_arguments.port)
1268 open_site = cmd_arguments.view
1269 production_env = cmd_arguments.production
1270 usermap_file = cmd_arguments.usermap
1272 cherrypy.config.update(
1274 "server.socket_host": ip,
1275 "server.socket_port": port,
1279 cherrypy.config.update({
"environment":
"production"})
1281 logging.info(f
"Server: Starting HTTP server on {ip}:{port}")
1284 webbrowser.open(
"http://" + ip +
":" + str(port))
1286 config_path = os.path.join(static_folder,
'../config/gl.cfg')
1290 gitlab_object =
None
1291 gitlab_config =
None
1293 if not os.path.exists(config_path):
1295 "ERROR: Expected to find config folder with Gitlab config,"
1296 f
" but {config_path} doesn't exist. "
1297 "Gitlab features will not work."
1300 gitlab_config = get_gitlab_config(config_path)
1301 gitlab_object = create_gitlab_object(config_path)
1303 update_linked_issues(gitlab_object, cwd_folder)
1304 gitlab_map = usermap_file
1306 f
"{gitlab_map} will be used to assign issues."
1309 cherrypy.quickstart(
1311 working_folder=cwd_folder,
1312 gitlab_object=gitlab_object,
1313 gitlab_config=gitlab_config,
1314 gitlab_map=gitlab_map,
1321if __name__ ==
"__main__":
def issue_redirect(self, iid)
contact
placeholder variable for contact
def check_comparison_status(self)
def create_comparison(self)
gitlab_config
Gitlab config.
last_restart
Date when this object was instantiated.
def comparisons(self, comparison_label=None)
def __init__(self, working_folder, gitlab_object, gitlab_config, gitlab_map)
def issue(self, file_path, rev_label, contact)
def revisions(self, revision_label=None)
gitlab_map
Gitlab usermap.
def retrieve_file_metadata(self, filename)
file_path
placeholder variable for path
working_folder
html folder that contains plots etc.
def update_issue(self, id, file_path, rev_label)
def create_issue(self, title, description)
gitlab_object
Gitlab object.
revision_label
placeholder variable for revision label
Optional[str] get_compact_git_hash(str repo_folder)
str get_file_metadata(str filename)
def clear_plots(str work_folder, List[str] keep_revisions)
List[str] get_popular_revision_combinations(str work_folder)
def get_html_folder(output_base_dir)
def get_html_plots_folder(output_base_dir)
def get_results_folder(output_base_dir)
def get_html_plots_tag_comparison_folder(output_base_dir, tags)