9 from typing
import Dict, Any, List, Tuple
15 from multiprocessing
import Process, Queue
32 from validationplots
import create_plots
33 import validationfunctions
36 g_plottingProcesses: Dict[str, Tuple[Process, Queue, Dict[str, Any]]] = ({})
39 def get_revision_label_from_json_filename(json_filename: str) -> str:
41 Gets the label of a revision from the path to the revision.json file
42 for example results/r121/revision.json
43 will result in the label r121
44 This is useful if the results folder has been moved by the user
46 folder_part = os.path.split(json_filename)[0]
47 last_folder = os.path.basename(folder_part)
52 def get_json_object_list(
53 results_folder: str, json_file_name: str
56 Searches one folder's sub-folder for json files of a
57 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
79 def 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)
90 def 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,
"")
98 def 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)
124 def 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()}."
133 def 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}")
178 Under here are functions that enable the validationserver to
179 interact directly with the Gitlab project page to track and
183 Config file with project information and access token in the local
184 machine. This is expected to be in the validation/config folder, in
185 the same root directory as the html_static files.
187 A check is performed to see if the config file exists with all the
188 relevant details and all of Gitlab functionalities are enabled/disabled
191 When the server is being set up, a Gitlab object is created, which
192 will be used subsequently to make all the API calls.
194 As a final server initialization step, the project is queried to
195 check if any of the current results are linked to existing issues
196 and the result files are updated accordingly.
198 The create/update issue functionality is accessible from the plot
199 container. All the relevant pages are part of the
200 validationserver cherry object.
202 Issues created by the validation server will contain a block of
203 automated code at the end of description and can be easily
204 filtered from the GitLab issues page using the search string
205 "Automated code, please do not delete". Relevant plots will be
206 listed as a note in the issue page, along with the revision label.
208 Function to upload files to Gitlab helps with pushing error plots
213 def 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)
230 def 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."
257 def get_project_object(
258 gitlab_object: gitlab.Gitlab, project_id: str
259 ) ->
'gitlab.project':
261 Fetch Gitlab project associated with the project ID.
264 gitlab project object
267 project = gitlab_object.projects.get(project_id, lazy=
True)
272 def search_project_issues(
273 gitlab_object: gitlab.Gitlab, search_term: str
274 ) ->
'list[gitlab.issues]':
276 Search in the Gitlab for open issues that contain the
280 gitlab project issues
283 issues = gitlab_object.issues.list(
292 def update_linked_issues(
293 gitlab_object: gitlab.Gitlab, cwd_folder: str
296 Fetch linked issues and update the comparison json files.
303 search_key =
"Automated code, please do not delete"
304 issues = search_project_issues(gitlab_object, search_key)
307 plot_issues = collections.defaultdict(list)
308 pattern =
r"Relevant plot: (\w+)"
310 match = re.search(pattern, issue.description)
312 plot_issues[match.groups()[0]].append(issue.iid)
315 rev_list = get_json_object_list(
317 validationpath.file_name_comparison_json,
321 comparison_json_path = os.path.join(
324 validationpath.file_name_comparison_json,
326 comparison_json = deliver_json(comparison_json_path)
327 for package
in comparison_json[
"packages"]:
328 for plotfile
in package.get(
"plotfiles"):
329 for plot
in plotfile.get(
"plots"):
330 if plot[
"title"]
in plot_issues.keys():
331 plot[
"issue"] = plot_issues[plot[
"title"]]
335 with open(comparison_json_path,
"w")
as jsonFile:
336 json.dump(comparison_json, jsonFile, indent=4)
339 def upload_file_gitlab(
340 file_path: str, project:
'gitlab.project'
343 Upload the passed file to the Gitlab project.
346 uploaded gitlab project file object
349 uploaded_file = project.upload(
350 file_path.split(
"/")[-1], filepath=file_path
356 def create_gitlab_issue(
359 uploaded_file: Dict[str, str],
360 project:
'gitlab.project'
363 Create a new project issue with the passed title and description, using
370 issue = project.issues.create({
"title": title,
"description": description})
372 issue.labels = [
"validation_issue"]
374 {
"body":
"See the [error plot]({})".format(uploaded_file[
"url"])}
379 logging.info(f
"Created a new Gitlab issue - {issue.iid}")
384 def update_gitlab_issue(
386 uploaded_file: Dict[str, str],
387 project:
'gitlab.project',
392 Update an existing project issue with the passed plotfile.
398 issue = project.issues.get(issue_iid)
399 plot_name = file_path.split(
"/")[-1].split(
".")[0]
400 plot_package = file_path.split(
"/")[-2]
403 "body":
"Related observation in validation of `{0}` package, `{1}`\
404 plot in `{2}` build. See the [error plot]({3})".format(
405 plot_package, plot_name, rev_label, uploaded_file[
"url"]
412 logging.info(f
"Updated existing Gitlab issue {issue.iid}")
418 Root Validation class to handle non-static HTTP requests into the
419 validation server. The two main functions are to hand out compiled json
420 objects of revisions and comparisons and to start and monitor the
421 creation of comparison plots.
425 def __init__(self, working_folder, gitlab_object, gitlab_config):
427 class initializer, which takes the path to the folders containing the
428 validation run results and plots (aka comparison), gitlab object and
440 os.environ[
"BELLE2_LOCAL_DIR"]
455 @cherrypy.tools.json_in()
456 @cherrypy.tools.json_out()
459 Triggers the start of a now comparison between the revisions supplied
462 rev_list = cherrypy.request.json[
"revision_list"]
463 logging.debug(
"Creating plots for revisions: " + str(rev_list))
464 progress_key = start_plotting_request(
468 return {
"progress_key": progress_key}
473 forward to the static landing page if
474 the default url is used (like http://localhost:8080/)
476 raise cherrypy.HTTPRedirect(
"/static/validation.html")
481 Serve file from the html/plot directory.
482 :param args: For the request /plots/a/b/c, these will be the strings
486 warn_wrong_directory()
489 raise cherrypy.HTTPError(404)
491 tag_folder = os.path.relpath(
497 path = os.path.join(tag_folder, *args[-2:])
498 return cherrypy.lib.static.serve_file(path)
501 @cherrypy.tools.json_in()
502 @cherrypy.tools.json_out()
505 Checks on the status of a comparison creation
507 progress_key = cherrypy.request.json[
"input"]
508 logging.debug(
"Checking status for plot creation: " + str(progress_key))
509 status = check_plotting_status(progress_key)
513 @cherrypy.tools.json_out()
516 Return a combined json object with all revisions and
517 mark the newest one with the field most_recent=true
521 rev_list = get_json_object_list(
523 validationpath.file_name_results_json,
528 reference_revision = json.loads(
534 full_path = os.path.join(
537 validationpath.file_name_results_json,
541 lbl_folder = get_revision_label_from_json_filename(full_path)
542 j = deliver_json(full_path)
543 j[
"label"] = lbl_folder
544 combined_list.append(j)
553 def sort_key(label: str):
556 f
"Misformatted label encountered: '{label}' "
557 f
"(doesn't seem to include date?)"
560 category, datetag = label.split(
"-", maxsplit=1)
561 print(category, datetag)
565 order = [
"release",
"prerelease",
"nightly"]
567 index = order.index(category)
571 f
"Misformatted label encountered: '{label}' (doesn't seem "
572 f
"to belong to any known category?)"
574 return f
"{index}-{datetag}"
576 combined_list.sort(key=
lambda rev: sort_key(rev[
"label"]), reverse=
True)
579 combined_list = [reference_revision] + combined_list
584 for r
in combined_list:
585 rdate_str = r[
"creation_date"]
586 if isinstance(rdate_str, str):
587 if len(rdate_str) > 0:
589 rdate = time.strptime(rdate_str,
"%Y-%m-%d %H:%M")
598 if newest_date
is None:
601 if rdate > newest_date:
605 for c
in combined_list:
606 if c[
"most_recent"]
is not None:
607 c[
"most_recent"] =
False
611 newest_rev[
"most_recent"] =
True
614 return {
"revisions": combined_list}
617 @cherrypy.tools.json_out()
620 return the json file of the comparison results of one specific
624 warn_wrong_directory()
638 if not os.path.isfile(path):
639 raise cherrypy.HTTPError(
640 404, f
"Json Comparison file {path} does not exist"
643 return deliver_json(path)
646 @cherrypy.tools.json_out()
650 JSON file containing git versions and time of last restart
653 warn_wrong_directory()
658 "last_restart": self.
last_restartlast_restart.strftime(
"%-d %b %H:%M ")
660 "version_restart": self.
versionversion,
662 os.environ[
"BELLE2_LOCAL_DIR"]
670 Metadata(str) of the file
672 cherrypy.response.headers[
'Content-Type'] =
'text/plain'
677 @cherrypy.tools.json_in()
678 @cherrypy.tools.json_out()
681 Call the functions to create the issue and redirect
682 to the created Gitlab issue page.
685 default_section = self.
gitlab_configgitlab_config[
'global'][
'default']
686 project_id = self.
gitlab_configgitlab_config[default_section][
'project_id']
688 project = get_project_object(self.
gitlab_objectgitlab_object, project_id)
689 uploaded_file = upload_file_gitlab(self.
plot_pathplot_path, project)
690 plot_title = self.
plot_pathplot_path.split(
"/")[-1].split(
".")[0]
691 description +=
"\n\n---\n\n:robot: Automated code, please do not delete\n\n\
692 Relevant plot: {0}\n\n\
693 Revision label: {1}\n\n---".format(
697 issue_id = create_gitlab_issue(
698 title, description, uploaded_file, project
703 comparison_json_path = os.path.join(
709 comparison_json = deliver_json(comparison_json_path)
710 for package
in comparison_json[
"packages"]:
711 if package[
"name"] == self.
plot_pathplot_path.split(
"/")[-2]:
712 for plotfile
in package.get(
"plotfiles"):
713 for plot
in plotfile.get(
"plots"):
716 == self.
plot_pathplot_path.split(
"/")[-1]
718 plot[
"issue"].append(issue_id)
721 with open(comparison_json_path,
"w")
as jsonFile:
722 json.dump(comparison_json, jsonFile, indent=4)
724 issue_url = self.
gitlab_configgitlab_config[default_section][
'project_url'] \
727 raise cherrypy.HTTPRedirect(
732 def issue(self, file_path, rev_label):
734 Return a template issue creation interface
735 for the user to add title and description.
744 return "ERROR: Gitlab integration not set up, verify config file."
746 raise cherrypy.HTTPRedirect(
"/static/validation_issue.html")
751 Redirect to the Gitlab issue page.
753 default_section = self.
gitlab_configgitlab_config[
'global'][
'default']
754 if not self.
gitlab_configgitlab_config[default_section][
'project_url']:
755 return "ERROR: Gitlab integration not set up, verify config file."
757 issue_url = self.
gitlab_configgitlab_config[default_section][
'project_url'] \
760 raise cherrypy.HTTPRedirect(
767 Update existing issue in Gitlab with current result plot
768 and redirect to the updated Gitlab issue page.
772 return "ERROR: Gitlab integration not set up, verify config file."
774 plot_path = os.path.join(
778 default_section = self.
gitlab_configgitlab_config[
'global'][
'default']
779 project_id = self.
gitlab_configgitlab_config[default_section][
'project_id']
780 project = get_project_object(self.
gitlab_objectgitlab_object, project_id)
781 uploaded_file = upload_file_gitlab(plot_path, project)
783 id, uploaded_file, project, plot_path, rev_label
787 issue_url = self.
gitlab_configgitlab_config[default_section][
'project_url'] \
791 raise cherrypy.HTTPRedirect(
796 def setup_gzip_compression(path, cherry_config):
798 enable GZip compression for all text-based content the
799 web-server will deliver
802 cherry_config[path].update(
804 "tools.gzip.on":
True,
805 "tools.gzip.mime_types": [
809 "application/javascript",
816 def get_argument_parser():
818 Prepare a parser for all the known command line arguments
822 parser = argparse.ArgumentParser()
828 help=
"The IP address on which the"
829 "server starts. Default is '127.0.0.1'.",
836 help=
"The port number on which"
837 " the server starts. Default is '8000'.",
844 help=
"Open validation website" " in the system's default browser.",
849 help=
"Run in production environment: "
850 "no log/error output via website and no auto-reload",
857 def parse_cmd_line_arguments():
859 Sets up a parser for command line arguments,
860 parses them and returns the arguments.
861 @return: An object containing the parsed command line arguments.
862 Arguments are accessed like they are attributes of the object,
863 i.e. [name_of_object].[desired_argument]
865 parser = get_argument_parser()
867 return parser.parse_args()
873 parse_command_line=False,
881 format=
"%(asctime)s %(levelname)-8s %(message)s",
886 cwd_folder = os.getcwd()
890 os.environ.get(
"BELLE2_RELEASE_DIR",
None)
is None
891 and os.environ.get(
"BELLE2_LOCAL_DIR",
None)
is None
893 sys.exit(
"Error: No basf2 release set up!")
895 cherry_config = dict()
897 cherry_config[
"/"] = {}
899 setup_gzip_compression(
"/", cherry_config)
902 static_folder_list = [
"validation",
"html_static"]
905 if basepath[
"central"]
is not None:
906 static_folder_central = os.path.join(
907 basepath[
"central"], *static_folder_list
909 if os.path.isdir(static_folder_central):
910 static_folder = static_folder_central
914 if basepath[
"local"]
is not None:
915 static_folder_local = os.path.join(
916 basepath[
"local"], *static_folder_list
918 if os.path.isdir(static_folder_local):
919 static_folder = static_folder_local
921 if static_folder
is None:
923 "Either BELLE2_RELEASE_DIR or BELLE2_LOCAL_DIR has to bet "
924 "to provide static HTML content. Did you run b2setup ?"
931 logging.info(f
"Serving static content from {static_folder}")
932 logging.info(f
"Serving result content and plots from {cwd_folder}")
935 if not os.path.isdir(results_folder):
937 "Result folder {} does not exist, run validate_basf2 first "
938 "to create validation output".format(results_folder)
943 os.path.isdir(os.path.join(results_folder, f))
944 for f
in os.listdir(results_folder)
947 if results_count == 0:
949 f
"Result folder {results_folder} contains no folders, run "
950 f
"validate_basf2 first to create validation output"
954 if not os.path.exists(
"html"):
958 if not os.path.exists(
"plots"):
962 cherry_config[
"/static"] = {
963 "tools.staticdir.on":
True,
965 "tools.staticdir.match":
r"^.*\.(js|css|html|png|js.map)$",
966 "tools.staticdir.dir": static_folder,
968 setup_gzip_compression(
"/static", cherry_config)
971 cherry_config[
"/plots"] = {
972 "tools.staticdir.on":
True,
974 "tools.staticdir.match":
r"^.*\.(png|json|pdf)$",
975 "tools.staticdir.dir": comparison_folder,
977 setup_gzip_compression(
"/plots", cherry_config)
980 cherry_config[
"/results"] = {
981 "tools.staticdir.on":
True,
982 "tools.staticdir.dir": results_folder,
984 "tools.staticdir.match":
r"^.*\.(log|root)$",
988 "tools.staticdir.content_types": {
989 "log":
"text/plain; charset=utf-8",
990 "root":
"application/octet-stream",
994 setup_gzip_compression(
"/results", cherry_config)
998 production_env =
False
999 if parse_command_line:
1001 cmd_arguments = parse_cmd_line_arguments()
1003 ip = cmd_arguments.ip
1004 port = int(cmd_arguments.port)
1005 open_site = cmd_arguments.view
1006 production_env = cmd_arguments.production
1008 cherrypy.config.update(
1010 "server.socket_host": ip,
1011 "server.socket_port": port,
1015 cherrypy.config.update({
"environment":
"production"})
1017 logging.info(f
"Server: Starting HTTP server on {ip}:{port}")
1020 webbrowser.open(
"http://" + ip +
":" + str(port))
1022 config_path = os.path.join(static_folder,
'../config/gl.cfg')
1026 gitlab_object =
None
1027 gitlab_config =
None
1028 if not os.path.exists(config_path):
1030 "ERROR: Expected to find config folder with Gitlab config,"
1031 f
" but {config_path} doesn't exist. "
1032 "Gitlab features will not work."
1035 gitlab_config = get_gitlab_config(config_path)
1036 gitlab_object = create_gitlab_object(config_path)
1038 update_linked_issues(gitlab_object, cwd_folder)
1040 cherrypy.quickstart(
1042 working_folder=cwd_folder,
1043 gitlab_object=gitlab_object,
1044 gitlab_config=gitlab_config,
1051 if __name__ ==
"__main__":
def issue_redirect(self, iid)
def check_comparison_status(self)
def create_comparison(self)
gitlab_config
Gitlab config.
last_restart
Date when this object was instantiated.
plot_path
placeholder variable for path
def comparisons(self, comparison_label=None)
def revisions(self, revision_label=None)
def retrieve_file_metadata(self, filename)
working_folder
html folder that contains plots etc.
def __init__(self, working_folder, gitlab_object, gitlab_config)
def update_issue(self, id, file_path, rev_label)
def create_issue(self, title, description)
gitlab_object
Gitlab object.
def issue(self, file_path, rev_label)
revision_label
placeholder variable for revision label
str get_file_metadata(str filename)
Optional[str] get_compact_git_hash(str repo_folder)
def get_html_plots_tag_comparison_folder(output_base_dir, tags)
def get_html_folder(output_base_dir)
def get_html_plots_folder(output_base_dir)
def get_results_folder(output_base_dir)