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
58 specific name and returns a combined list of the
62 search_string = results_folder +
"/*/" + json_file_name
64 found_revs = glob(search_string)
67 for r_file
in found_revs:
69 with open(r_file)
as json_file:
70 data = json.load(json_file)
73 found_rev_labels.append(
74 get_revision_label_from_json_filename(r_file)
77 return found_rev_labels
80def deliver_json(file_name: str):
82 Simply load & parse a json file and return the
86 with open(file_name)
as json_file:
87 data = json.load(json_file)
91def create_revision_key(revision_names: List[str]) -> str:
93 Create a string key out of a revision list, which is handed to tho browser
94 in form of a progress key
96 return functools.reduce(
lambda x, y: x +
"-" + y, revision_names,
"")
99def check_plotting_status(progress_key: str):
101 Check the plotting status via the supplied progress_key
104 if progress_key
not in g_plottingProcesses:
107 process, qu, last_status = g_plottingProcesses[progress_key]
112 while not qu.empty():
113 msg = qu.get_nowait()
117 g_plottingProcesses[progress_key] = (process, qu, last_status)
125def warn_wrong_directory():
126 if not os.getcwd().endswith(
"html"):
128 f
"ERROR: Expected to be in HTML directory, but my current "
129 f
"working directory is {os.getcwd()}; abspath: {os.getcwd()}."
134def start_plotting_request(
135 revision_names: List[str], results_folder: str
138 Start a new comparison between the supplied revisions
144 rev_key = create_revision_key(revision_names)
147 if rev_key
in g_plottingProcesses:
148 logging.info(f
"Plotting request for {rev_key} still running")
165 os.path.dirname(results_folder),
169 g_plottingProcesses[rev_key] = (p, qu,
None)
171 logging.info(f
"Started process for plotting request {rev_key}")
179Under here are functions that enable the validationserver to
180interact directly with the Gitlab project page to track and
184Config file with project information and access token in the local
185machine. This is expected to be in the validation/config folder, in
186the same root directory as the html_static files.
188A check is performed to see if the config file exists with all the
189relevant details and all of Gitlab functionalities are enabled/disabled
192When the server is being set up, a Gitlab object is created, which
193will be used subsequently to make all the API calls.
195As a final server initialization step, the project is queried to
196check if any of the current results are linked to existing issues
197and the result files are updated accordingly.
199The create/update issue functionality is accessible from the plot
200container. All the relevant pages are part of the
201validationserver cherry object.
203Issues created by the validation server will contain a block of
204automated code at the end of description and can be easily
205filtered from the GitLab issues page using the search string
206"Automated code, please do not delete". Relevant plots will be
207listed as a note in the issue page, along with the revision label.
209Function to upload files to Gitlab helps with pushing error plots
214def get_gitlab_config(
216) -> configparser.ConfigParser:
218 Parse the configuration file to be used to authenticate
219 GitLab API and retrieve relevant project info.
222 gitlab configparser object
225 gitlab_config = configparser.ConfigParser()
226 gitlab_config.read(config_path)
231def create_gitlab_object(config_path: str) -> gitlab.Gitlab:
233 Establish connection with Gitlab using a private access key and return
234 a Gitlab object that can be used to make API calls. Default config
235 from the passed ini file will be used.
241 gitlab_object = gitlab.Gitlab.from_config(
242 config_files=[config_path]
246 logging.info(
"Established connection with Gitlab")
247 except gitlab.exceptions.GitlabAuthenticationError:
250 "Issue with authenticating GitLab. "
251 "Please ensure access token is correct and valid. "
252 "GitLab Integration will be disabled."
254 except requests.exceptions.Timeout:
257 "GitLab servers feeling under the weather, DESY outage? "
258 "GitLab Integration will be disabled."
264def get_project_object(
265 gitlab_object: gitlab.Gitlab, project_id: str
266) ->
'gitlab.project':
268 Fetch Gitlab project associated with the project ID.
271 gitlab project object
274 project = gitlab_object.projects.get(project_id, lazy=
True)
279def search_project_issues(
280 gitlab_object: gitlab.Gitlab,
282 state: str =
'opened',
283) ->
'list[gitlab.issues]':
285 Search in the Gitlab for open issues that contain the
289 gitlab project issues
292 issues = gitlab_object.issues.list(
303def update_linked_issues(
304 gitlab_object: gitlab.Gitlab, cwd_folder: str
307 Fetch linked issues and update the comparison json files.
314 search_key =
"Automated code, please do not delete"
315 issues = search_project_issues(gitlab_object, search_key)
316 past_issues = search_project_issues(gitlab_object, search_key,
'closed')
320 plot_issues = collections.defaultdict(list)
321 script_issues = collections.defaultdict(list)
322 pattern =
r"Relevant ([a-z]+): (\w+.*\w*)"
323 for i, issue
in enumerate(issues+past_issues):
324 match = re.search(pattern, issue.description)
326 if match.groups()[0] ==
'plot':
328 plot_issues[match.groups()[1]].append(-issue.iid)
330 plot_issues[match.groups()[1]].append(issue.iid)
332 script_issues[match.groups()[1]].append(issue.iid)
335 rev_list = get_json_object_list(
337 validationpath.file_name_comparison_json,
341 comparison_json_path = os.path.join(
344 validationpath.file_name_comparison_json,
346 comparison_json = deliver_json(comparison_json_path)
347 for package
in comparison_json[
"packages"]:
348 for plotfile
in package.get(
"plotfiles"):
349 for plot
in plotfile.get(
"plots"):
350 if plot[
"png_filename"]
in plot_issues.keys():
351 plot[
"issue"] = plot_issues[plot[
"png_filename"]]
355 with open(comparison_json_path,
"w")
as jsonFile:
356 json.dump(comparison_json, jsonFile, indent=4)
359 rev_list = get_json_object_list(
361 validationpath.file_name_results_json,
364 revision_json_path = os.path.join(
367 validationpath.file_name_results_json,
369 revision_json = deliver_json(revision_json_path)
370 for package
in revision_json[
"packages"]:
371 for scriptfile
in package.get(
"scriptfiles"):
372 if scriptfile[
"name"]
in script_issues.keys():
373 scriptfile[
"issues"] = script_issues[scriptfile[
'name']]
375 scriptfile[
"issues"] = []
377 with open(revision_json_path,
"w")
as jsonFile:
378 json.dump(revision_json, jsonFile, indent=4)
381def upload_file_gitlab(
382 file_path: str, project:
'gitlab.project'
385 Upload the passed file to the Gitlab project.
388 uploaded gitlab project file object
391 uploaded_file = project.upload(
392 file_path.split(
"/")[-1], filepath=file_path
398def get_librarians(package: str) -> List[str]:
400 Function to get package librarian(s)' GitLab usernames. Temp solution
401 until the .librarians file directly provides Gitlab usernames.
404 list of librarians' Gitlab usernames
408 librarian_file = os.path.join(
414 with open(librarian_file,
'r')
as f:
415 librarians = f.readlines()
416 except FileNotFoundError:
418 f
"{librarian_file} couldn't be found. Corrupted package/librarian file?"
423 desy_map_path = os.path.join(
424 "/home/b2soft/gitlab",
427 spec = importlib.util.spec_from_file_location(
'account_map', desy_map_path)
428 desy_map = importlib.util.module_from_spec(spec)
430 spec.loader.exec_module(desy_map)
431 except FileNotFoundError:
433 f
"{desy_map_path} couldn't be found. Have you setup Gitlab Webhook?"
437 for librarian
in librarians:
438 usernames.append(desy_map.get_gitlab_account(librarian.rstrip()))
447 gitlab_object: gitlab.Gitlab
450 Parse string to find email id(s) and then match them with their Gitlab ids
451 using the userid map.
454 Dictionary with list of Gitlab IDs and corresponding list of
458 email_regex = re.compile(
r"([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*"
459 r"|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([-!#-'*+/-9="
460 r"?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]"
463 email_ids = re.finditer(email_regex, contact)
470 with open(map_file,
'r')
as f:
471 id_map = f.readlines()
472 except FileNotFoundError:
474 f
"{map_file} couldn't be found. Did you get the location right?"
478 for email
in email_ids:
481 (line
for line
in id_map
if email.group()
in line),
None
485 f
"No userid found for {email} in the map, could it be that "
486 "they are (sadly) no longer in the collaboration?"
489 username = match.split(
' ')[1].rstrip()
490 assignees[
'usernames'].append(username)
493 f
"Map info {match} does not match the required format for map "
494 "'email gitlab_username'."
499 if not assignees[
'usernames']:
500 assignees[
'usernames'] = get_librarians(package)
502 "Couldn't find contact/id so assigning issue to the"
503 " package librarians."
506 for user
in assignees[
'usernames']:
508 assignees[
'gitlab_ids'].append(
509 gitlab_object.users.list(username=user)[0].id
513 f
"Could not find {user} in Gitlab."
517 "Issue will be assigned to "
518 f
"{[gitlab_object.users.get(id) for id in assignees['gitlab_ids']]}."
526def create_gitlab_issue(
529 uploaded_file: Dict[str, str],
530 assignees: Dict[str, List],
532 project:
'gitlab.project'
535 Create a new project issue with the passed title, description and package,
542 issue = project.issues.create({
"title": title,
543 "description": description,
544 "labels": [package,
'validation_issue']})
546 issue_note = issue.notes.create(
547 {
"body": f
'View the [error plot/log file]({uploaded_file["url"]}).'}
550 issue.assignee_ids = assignees[
'gitlab_ids']
553 if len(assignees[
'gitlab_ids']) > 1:
554 related_users = [f
'@{user} ' for user
in assignees[
'usernames'][1:]]
555 issue_note.body += f
"\n\nPinging {' '.join(related_users)}"
560 logging.info(f
"Created a new Gitlab issue - {issue.iid}")
565def update_gitlab_issue(
567 uploaded_file: Dict[str, str],
568 project:
'gitlab.project',
573 Update an existing project issue with the passed plotfile.
579 issue = project.issues.get(issue_iid)
580 name = file_path.split(
"/")[-1].split(
".")[0]
581 package = file_path.split(
"/")[-2]
584 if 'log' == file_path.split(
"/")[-1].split(
".")[-1]:
585 issue_type =
'script'
588 "body": f
'Related observation in validation of `{package}` package, `{name}`' +
589 f
'{issue_type} in `{rev_label}` build. View the [error plot/log file]({uploaded_file["url"]}).'
595 logging.info(f
"Updated existing Gitlab issue {issue.iid}")
598def update_scriptfile_issues_json(
599 revision_json_path: str,
600 scritptfile_name: str,
601 scritptfile_package: str,
605 Update the scriptfile's linked issues key in the relevant revision's
612 revision_json = deliver_json(revision_json_path)
613 for package
in revision_json[
"packages"]:
614 if package[
"name"] == scritptfile_package:
615 for scriptfile
in package.get(
"scriptfiles"):
616 if (scriptfile[
"name"] == scritptfile_name):
617 scriptfile[
"issues"].append(issue_id)
620 with open(revision_json_path,
"w")
as jsonFile:
621 json.dump(revision_json, jsonFile, indent=4)
624def update_plot_issues_json(
625 comparison_json_path: str,
631 Update the plotfile's linked issues key in the relevant comparison
638 comparison_json = deliver_json(comparison_json_path)
639 for package
in comparison_json[
"packages"]:
640 if package[
"name"] == plot_package:
641 for plotfile
in package.get(
"plotfiles"):
642 for plot
in plotfile.get(
"plots"):
643 if (plot[
"png_filename"] == plot_name):
644 plot[
"issue"].append(issue_id)
647 with open(comparison_json_path,
"w")
as jsonFile:
648 json.dump(comparison_json, jsonFile, indent=4)
654 Root Validation class to handle non-static HTTP requests into the
655 validation server. The two main functions are to hand out compiled json
656 objects of revisions and comparisons and to start and monitor the
657 creation of comparison plots.
661 def __init__(self, working_folder, gitlab_object, gitlab_config, gitlab_map):
663 class initializer, which takes the path to the folders containing the
664 validation run results and plots (aka comparison), gitlab object and
676 os.environ[
"BELLE2_LOCAL_DIR"]
696 @cherrypy.tools.json_in()
697 @cherrypy.tools.json_out()
700 Triggers the start of a now comparison between the revisions supplied
703 rev_list = cherrypy.request.json[
"revision_list"]
704 logging.debug(
"Creating plots for revisions: " + str(rev_list))
705 progress_key = start_plotting_request(
709 return {
"progress_key": progress_key}
714 forward to the static landing page if
715 the default url is used (like http://localhost:8080/)
717 raise cherrypy.HTTPRedirect(
"/static/validation.html")
722 Serve file from the html/plot directory.
723 :param args: For the request /plots/a/b/c, these will be the strings
727 warn_wrong_directory()
730 raise cherrypy.HTTPError(404)
732 tag_folder = os.path.relpath(
738 path = os.path.join(tag_folder, *args[-2:])
739 return cherrypy.lib.static.serve_file(path)
742 @cherrypy.tools.json_in()
743 @cherrypy.tools.json_out()
746 Checks on the status of a comparison creation
748 progress_key = cherrypy.request.json[
"input"]
749 logging.debug(
"Checking status for plot creation: " + str(progress_key))
750 status = check_plotting_status(progress_key)
754 @cherrypy.tools.json_out()
757 Return a combined json object with all revisions and
758 mark the newest one with the field most_recent=true
762 rev_list = get_json_object_list(
764 validationpath.file_name_results_json,
769 reference_revision = json.loads(
775 full_path = os.path.join(
778 validationpath.file_name_results_json,
782 lbl_folder = get_revision_label_from_json_filename(full_path)
783 j = deliver_json(full_path)
784 j[
"label"] = lbl_folder
785 combined_list.append(j)
794 def sort_key(label: str):
797 f
"Misformatted label encountered: '{label}' "
798 f
"(doesn't seem to include date?)"
801 category, datetag = label.split(
"-", maxsplit=1)
802 print(category, datetag)
806 order = [
"release",
"prerelease",
"nightly"]
808 index = order.index(category)
812 f
"Misformatted label encountered: '{label}' (doesn't seem "
813 f
"to belong to any known category?)"
815 return f
"{index}-{datetag}"
817 combined_list.sort(key=
lambda rev: sort_key(rev[
"label"]), reverse=
True)
820 combined_list = [reference_revision] + combined_list
825 for r
in combined_list:
826 rdate_str = r[
"creation_date"]
827 if isinstance(rdate_str, str):
828 if len(rdate_str) > 0:
830 rdate = time.strptime(rdate_str,
"%Y-%m-%d %H:%M")
839 if newest_date
is None:
842 if rdate > newest_date:
846 for c
in combined_list:
847 if c[
"most_recent"]
is not None:
848 c[
"most_recent"] =
False
852 newest_rev[
"most_recent"] =
True
855 return {
"revisions": combined_list}
858 @cherrypy.tools.json_out()
861 return the json file of the comparison results of one specific
865 warn_wrong_directory()
879 if not os.path.isfile(path):
880 raise cherrypy.HTTPError(
881 404, f
"Json Comparison file {path} does not exist"
884 return deliver_json(path)
887 @cherrypy.tools.json_out()
891 JSON file containing git versions and time of last restart
894 warn_wrong_directory()
899 "last_restart": self.
last_restart.strftime(
"%-d %b %H:%M ")
901 "version_restart": self.
version,
903 os.environ[
"BELLE2_LOCAL_DIR"]
911 Metadata(str) of the file
913 cherrypy.response.headers[
'Content-Type'] =
'text/plain'
918 @cherrypy.tools.json_in()
919 @cherrypy.tools.json_out()
922 Call the functions to create the issue and redirect
923 to the created Gitlab issue page.
928 if 'log' == self.
file_path.split(
"/")[-1].split(
".")[-1]:
929 issue_type =
'script'
931 project_id = self.
gitlab_config[default_section][
'project_id']
934 uploaded_file = upload_file_gitlab(self.
file_path, project)
939 file_name = self.
file_path.split(
"/")[-1].split(
".log")[0]
940 file_package = self.
file_path.split(
"/")[-2]
942 assignees = parse_contact(
945 description +=
"\n\n---\n\n:robot: Automated code, please do not delete\n\n" + \
946 f
"Relevant {issue_type}: {file_name}\n\n" + \
947 f
"Revision label: {self.revision_label}\n\n---"
948 issue_id = create_gitlab_issue(
949 title, description, uploaded_file, assignees, file_package, project
956 if issue_type ==
'script':
957 revision_json_path = os.path.join(
960 validationpath.file_name_results_json,
962 update_scriptfile_issues_json(
963 revision_json_path, file_name, file_package, issue_id)
965 comparison_json_path = os.path.join(
970 update_plot_issues_json(
971 comparison_json_path, file_name, file_package, issue_id)
973 issue_url = self.
gitlab_config[default_section][
'project_url'] \
976 raise cherrypy.HTTPRedirect(
981 def issue(self, file_path, rev_label, contact):
983 Return a template issue creation interface
984 for the user to add title and description.
994 return "ERROR: Gitlab integration not set up, verify config file."
996 raise cherrypy.HTTPRedirect(
"/static/validation_issue.html")
1001 Redirect to the Gitlab issue page.
1005 return "ERROR: Gitlab integration not set up, verify config file."
1007 issue_url = self.
gitlab_config[default_section][
'project_url'] \
1010 raise cherrypy.HTTPRedirect(
1017 Update existing issue in Gitlab with current result plot
1018 and redirect to the updated Gitlab issue page.
1022 return "ERROR: Gitlab integration not set up, verify config file."
1024 plot_path = os.path.join(
1029 project_id = self.
gitlab_config[default_section][
'project_id']
1030 project = get_project_object(self.
gitlab_object, project_id)
1031 uploaded_file = upload_file_gitlab(plot_path, project)
1032 update_gitlab_issue(
1033 id, uploaded_file, project, plot_path, rev_label
1037 issue_url = self.
gitlab_config[default_section][
'project_url'] \
1041 raise cherrypy.HTTPRedirect(
1046def setup_gzip_compression(path, cherry_config):
1048 enable GZip compression for all text-based content the
1049 web-server will deliver
1052 cherry_config[path].update(
1054 "tools.gzip.on":
True,
1055 "tools.gzip.mime_types": [
1059 "application/javascript",
1066def get_argument_parser():
1068 Prepare a parser for all the known command line arguments
1072 parser = argparse.ArgumentParser()
1075 parser.add_argument(
1078 help=
"The IP address on which the"
1079 "server starts. Default is '127.0.0.1'.",
1081 default=
"127.0.0.1",
1083 parser.add_argument(
1086 help=
"The port number on which"
1087 " the server starts. Default is '8000'.",
1091 parser.add_argument(
1094 help=
"Open validation website" " in the system's default browser.",
1095 action=
"store_true",
1097 parser.add_argument(
1099 help=
"Run in production environment: "
1100 "no log/error output via website and no auto-reload",
1101 action=
"store_true",
1103 parser.add_argument(
1106 help=
"Path of file containing <email gitlab_username> map.",
1114def parse_cmd_line_arguments():
1116 Sets up a parser for command line arguments,
1117 parses them and returns the arguments.
1118 @return: An object containing the parsed command line arguments.
1119 Arguments are accessed like they are attributes of the object,
1120 i.e. [name_of_object].[desired_argument]
1122 parser = get_argument_parser()
1124 return parser.parse_args()
1130 parse_command_line=False,
1136 logging.basicConfig(
1137 level=logging.DEBUG,
1138 format=
"%(asctime)s %(levelname)-8s %(message)s",
1143 cwd_folder = os.getcwd()
1147 os.environ.get(
"BELLE2_RELEASE_DIR",
None)
is None
1148 and os.environ.get(
"BELLE2_LOCAL_DIR",
None)
is None
1150 sys.exit(
"Error: No basf2 release set up!")
1152 cherry_config = dict()
1154 cherry_config[
"/"] = {}
1156 setup_gzip_compression(
"/", cherry_config)
1159 static_folder_list = [
"validation",
"html_static"]
1160 static_folder =
None
1162 if basepath[
"central"]
is not None:
1163 static_folder_central = os.path.join(
1164 basepath[
"central"], *static_folder_list
1166 if os.path.isdir(static_folder_central):
1167 static_folder = static_folder_central
1171 if basepath[
"local"]
is not None:
1172 static_folder_local = os.path.join(
1173 basepath[
"local"], *static_folder_list
1175 if os.path.isdir(static_folder_local):
1176 static_folder = static_folder_local
1178 if static_folder
is None:
1180 "Either BELLE2_RELEASE_DIR or BELLE2_LOCAL_DIR has to set "
1181 "to provide static HTML content. Did you run b2setup ?"
1188 logging.info(f
"Serving static content from {static_folder}")
1189 logging.info(f
"Serving result content and plots from {cwd_folder}")
1192 if not os.path.isdir(results_folder):
1194 f
"Result folder {results_folder} does not exist, run validate_basf2 first " +
1195 "to create validation output"
1198 results_count = sum(
1200 os.path.isdir(os.path.join(results_folder, f))
1201 for f
in os.listdir(results_folder)
1204 if results_count == 0:
1206 f
"Result folder {results_folder} contains no folders, run "
1207 f
"validate_basf2 first to create validation output"
1211 if not os.path.exists(
"html"):
1215 if not os.path.exists(
"plots"):
1218 if os.path.exists(
"plots/rainbow.json"):
1219 logging.info(
"Removing old plots and unpopular combinations")
1226 cherry_config[
"/static"] = {
1227 "tools.staticdir.on":
True,
1229 "tools.staticdir.match":
r"^.*\.(js|css|html|png|js.map)$",
1230 "tools.staticdir.dir": static_folder,
1232 setup_gzip_compression(
"/static", cherry_config)
1235 cherry_config[
"/plots"] = {
1236 "tools.staticdir.on":
True,
1238 "tools.staticdir.match":
r"^.*\.(png|json|pdf)$",
1239 "tools.staticdir.dir": comparison_folder,
1241 setup_gzip_compression(
"/plots", cherry_config)
1244 cherry_config[
"/results"] = {
1245 "tools.staticdir.on":
True,
1246 "tools.staticdir.dir": results_folder,
1248 "tools.staticdir.match":
r"^.*\.(log|root|txt)$",
1252 "tools.staticdir.content_types": {
1253 "log":
"text/plain; charset=utf-8",
1254 "root":
"application/octet-stream",
1258 setup_gzip_compression(
"/results", cherry_config)
1262 production_env =
False
1263 if parse_command_line:
1265 cmd_arguments = parse_cmd_line_arguments()
1267 ip = cmd_arguments.ip
1268 port = int(cmd_arguments.port)
1269 open_site = cmd_arguments.view
1270 production_env = cmd_arguments.production
1271 usermap_file = cmd_arguments.usermap
1273 cherrypy.config.update(
1275 "server.socket_host": ip,
1276 "server.socket_port": port,
1280 cherrypy.config.update({
"environment":
"production"})
1282 logging.info(f
"Server: Starting HTTP server on {ip}:{port}")
1285 webbrowser.open(
"http://" + ip +
":" + str(port))
1287 config_path = os.path.join(static_folder,
'../config/gl.cfg')
1291 gitlab_object =
None
1292 gitlab_config =
None
1294 if not os.path.exists(config_path):
1296 "ERROR: Expected to find config folder with Gitlab config,"
1297 f
" but {config_path} doesn't exist. "
1298 "Gitlab features will not work."
1301 gitlab_config = get_gitlab_config(config_path)
1302 gitlab_object = create_gitlab_object(config_path)
1304 update_linked_issues(gitlab_object, cwd_folder)
1305 gitlab_map = usermap_file
1307 f
"{gitlab_map} will be used to assign issues."
1310 cherrypy.quickstart(
1312 working_folder=cwd_folder,
1313 gitlab_object=gitlab_object,
1314 gitlab_config=gitlab_config,
1315 gitlab_map=gitlab_map,
1322if __name__ ==
"__main__":
create_issue(self, title, description)
update_issue(self, id, file_path, rev_label)
contact
placeholder variable for contact
gitlab_config
Gitlab config.
last_restart
Date when this object was instantiated.
revisions(self, revision_label=None)
issue_redirect(self, iid)
retrieve_file_metadata(self, filename)
comparisons(self, comparison_label=None)
gitlab_map
Gitlab usermap.
file_path
placeholder variable for path
issue(self, file_path, rev_label, contact)
check_comparison_status(self)
working_folder
html folder that contains plots etc.
__init__(self, working_folder, gitlab_object, gitlab_config, gitlab_map)
gitlab_object
Gitlab object.
revision_label
placeholder variable for revision label
clear_plots(str work_folder, List[str] keep_revisions)
Optional[str] get_compact_git_hash(str repo_folder)
str get_file_metadata(str filename)
List[str] get_popular_revision_combinations(str work_folder)
get_html_folder(output_base_dir)
get_html_plots_folder(output_base_dir)
get_html_plots_tag_comparison_folder(output_base_dir, tags)
get_results_folder(output_base_dir)