Belle II Software  release-08-01-10
validationserver.py
1 
9 from typing import Dict, Any, List, Tuple
10 from glob import glob
11 import json
12 import functools
13 import time
14 import datetime
15 from multiprocessing import Process, Queue
16 import os.path
17 import argparse
18 import logging
19 import sys
20 import queue
21 import webbrowser
22 import re
23 import collections
24 import configparser
25 
26 # 3rd
27 import cherrypy
28 import gitlab
29 
30 # ours
31 import json_objects
32 from validationplots import create_plots
33 import validationfunctions
34 import validationpath
35 
36 g_plottingProcesses: Dict[str, Tuple[Process, Queue, Dict[str, Any]]] = ({})
37 
38 
39 def get_revision_label_from_json_filename(json_filename: str) -> str:
40  """
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
45  """
46  folder_part = os.path.split(json_filename)[0]
47  last_folder = os.path.basename(folder_part)
48 
49  return last_folder
50 
51 
52 def get_json_object_list(
53  results_folder: str, json_file_name: str
54 ) -> List[str]:
55  """
56  Searches one folder's sub-folder for json files of a
57  specific name and returns a combined list of the
58  json file's content
59  """
60 
61  search_string = results_folder + "/*/" + json_file_name
62 
63  found_revs = glob(search_string)
64  found_rev_labels = []
65 
66  for r_file in found_revs:
67  # try loading json file
68  with open(r_file) as json_file:
69  data = json.load(json_file) # noqa
70 
71  # always use the folder name as label
72  found_rev_labels.append(
73  get_revision_label_from_json_filename(r_file)
74  )
75 
76  return found_rev_labels
77 
78 
79 def deliver_json(file_name: str):
80  """
81  Simply load & parse a json file and return the
82  python objects
83  """
84 
85  with open(file_name) as json_file:
86  data = json.load(json_file)
87  return data
88 
89 
90 def create_revision_key(revision_names: List[str]) -> str:
91  """
92  Create a string key out of a revision list, which is handed to tho browser
93  in form of a progress key
94  """
95  return functools.reduce(lambda x, y: x + "-" + y, revision_names, "")
96 
97 
98 def check_plotting_status(progress_key: str):
99  """
100  Check the plotting status via the supplied progress_key
101  """
102 
103  if progress_key not in g_plottingProcesses:
104  return None
105 
106  process, qu, last_status = g_plottingProcesses[progress_key]
107 
108  # read latest message
109  try:
110  # read as much entries from the queue as possible
111  while not qu.empty():
112  msg = qu.get_nowait()
113  last_status = msg
114 
115  # update the last status
116  g_plottingProcesses[progress_key] = (process, qu, last_status)
117  except queue.Empty:
118  pass
119 
120  return last_status
121 
122 
123 # todo: remove this, once we're certain that the bug was fixed!
124 def warn_wrong_directory():
125  if not os.getcwd().endswith("html"):
126  print(
127  f"ERROR: Expected to be in HTML directory, but my current "
128  f"working directory is {os.getcwd()}; abspath: {os.getcwd()}."
129  )
130 
131 
132 # todo: limit the number of running plotting requests & terminate hanging ones
133 def start_plotting_request(
134  revision_names: List[str], results_folder: str
135 ) -> str:
136  """
137  Start a new comparison between the supplied revisions
138 
139  Returns:
140  revision key
141  """
142 
143  rev_key = create_revision_key(revision_names)
144 
145  # still running a plotting for this combination ?
146  if rev_key in g_plottingProcesses:
147  logging.info(f"Plotting request for {rev_key} still running")
148  return rev_key
149 
150  # create queue to stream progress, only one directional from parent to
151  # child
152  qu = Queue()
153 
154  # start a new process for creating the plots
155  p = Process(
156  target=create_plots,
157  args=(
158  revision_names,
159  False,
160  qu,
161  # go one folder up, because this function
162  # expects the work dir, which contains
163  # the results folder
164  os.path.dirname(results_folder),
165  ),
166  )
167  p.start()
168  g_plottingProcesses[rev_key] = (p, qu, None)
169 
170  logging.info(f"Started process for plotting request {rev_key}")
171 
172  return rev_key
173 
174 
175 """
176 Gitlab Integration
177 
178 Under here are functions that enable the validationserver to
179 interact directly with the Gitlab project page to track and
180 update issues.
181 
182 Requirement:
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.
186 
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
189 accordingly.
190 
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.
193 
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.
197 
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.
201 
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.
207 
208 Function to upload files to Gitlab helps with pushing error plots
209 to the project.
210 """
211 
212 
213 def get_gitlab_config(
214  config_path: str
215 ) -> configparser.ConfigParser:
216  """
217  Parse the configuration file to be used to authenticate
218  GitLab API and retrieve relevant project info.
219 
220  Returns:
221  gitlab configparser object
222  """
223 
224  gitlab_config = configparser.ConfigParser()
225  gitlab_config.read(config_path)
226 
227  return gitlab_config
228 
229 
230 def create_gitlab_object(config_path: str) -> gitlab.Gitlab:
231  """
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.
235 
236  Returns:
237  gitlab object
238  """
239 
240  gitlab_object = gitlab.Gitlab.from_config(
241  config_files=[config_path]
242  )
243  try:
244  gitlab_object.auth()
245  logging.info("Established connection with Gitlab")
246  except gitlab.exceptions.GitlabAuthenticationError:
247  gitlab_object = None
248  logging.warning(
249  "Issue with authenticating GitLab. "
250  "Please ensure access token is correct and valid. "
251  "GitLab Integration will be disabled."
252  )
253 
254  return gitlab_object
255 
256 
257 def get_project_object(
258  gitlab_object: gitlab.Gitlab, project_id: str
259 ) -> 'gitlab.project':
260  """
261  Fetch Gitlab project associated with the project ID.
262 
263  Returns:
264  gitlab project object
265  """
266 
267  project = gitlab_object.projects.get(project_id, lazy=True)
268 
269  return project
270 
271 
272 def search_project_issues(
273  gitlab_object: gitlab.Gitlab, search_term: str
274 ) -> 'list[gitlab.issues]':
275  """
276  Search in the Gitlab for open issues that contain the
277  key phrase.
278 
279  Returns:
280  gitlab project issues
281  """
282 
283  issues = gitlab_object.issues.list(
284  search=search_term,
285  state='opened',
286  lazy=True,
287  )
288 
289  return issues
290 
291 
292 def update_linked_issues(
293  gitlab_object: gitlab.Gitlab, cwd_folder: str
294 ) -> None:
295  """
296  Fetch linked issues and update the comparison json files.
297 
298  Returns:
299  None
300  """
301 
302  # collect list of issues validation server has worked with
303  search_key = "Automated code, please do not delete"
304  issues = search_project_issues(gitlab_object, search_key)
305 
306  # find out the plots linked to the issues
307  plot_issues = collections.defaultdict(list)
308  pattern = r"Relevant plot: (\w+)"
309  for issue in issues:
310  match = re.search(pattern, issue.description)
311  if match:
312  plot_issues[match.groups()[0]].append(issue.iid)
313 
314  # get list of available revision
315  rev_list = get_json_object_list(
317  validationpath.file_name_comparison_json,
318  )
319 
320  for r in rev_list:
321  comparison_json_path = os.path.join(
323  r,
324  validationpath.file_name_comparison_json,
325  )
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"]]
332  else:
333  plot["issue"] = []
334 
335  with open(comparison_json_path, "w") as jsonFile:
336  json.dump(comparison_json, jsonFile, indent=4)
337 
338 
339 def upload_file_gitlab(
340  file_path: str, project: 'gitlab.project'
341 ) -> Dict[str, str]:
342  """
343  Upload the passed file to the Gitlab project.
344 
345  Returns:
346  uploaded gitlab project file object
347  """
348 
349  uploaded_file = project.upload(
350  file_path.split("/")[-1], filepath=file_path
351  )
352 
353  return uploaded_file
354 
355 
356 def create_gitlab_issue(
357  title: str,
358  description: str,
359  uploaded_file: Dict[str, str],
360  project: 'gitlab.project'
361 ) -> str:
362  """
363  Create a new project issue with the passed title and description, using
364  Gitlab API.
365 
366  Returns:
367  created isssue id
368  """
369 
370  issue = project.issues.create({"title": title, "description": description})
371 
372  issue.labels = ["validation_issue"]
373  issue.notes.create(
374  {"body": "See the [error plot]({})".format(uploaded_file["url"])}
375  )
376 
377  issue.save()
378 
379  logging.info(f"Created a new Gitlab issue - {issue.iid}")
380 
381  return issue.iid
382 
383 
384 def update_gitlab_issue(
385  issue_iid: str,
386  uploaded_file: Dict[str, str],
387  project: 'gitlab.project',
388  file_path: str,
389  rev_label: str
390 ) -> None:
391  """
392  Update an existing project issue with the passed plotfile.
393 
394  Returns:
395  None
396  """
397 
398  issue = project.issues.get(issue_iid)
399  plot_name = file_path.split("/")[-1].split(".")[0]
400  plot_package = file_path.split("/")[-2]
401  issue.notes.create(
402  {
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"]
406  )
407  }
408  )
409 
410  issue.save()
411 
412  logging.info(f"Updated existing Gitlab issue {issue.iid}")
413 
414 
416 
417  """
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.
422 
423  """
424 
425  def __init__(self, working_folder, gitlab_object, gitlab_config):
426  """
427  class initializer, which takes the path to the folders containing the
428  validation run results and plots (aka comparison), gitlab object and
429  config
430  """
431 
432 
433  self.working_folderworking_folder = working_folder
434 
435 
436  self.last_restartlast_restart = datetime.datetime.now()
437 
438 
440  os.environ["BELLE2_LOCAL_DIR"]
441  )
442 
443 
444  self.gitlab_objectgitlab_object = gitlab_object
445 
446 
447  self.gitlab_configgitlab_config = gitlab_config
448 
449 
450  self.plot_pathplot_path = None
451 
452  self.revision_labelrevision_label = None
453 
454  @cherrypy.expose
455  @cherrypy.tools.json_in()
456  @cherrypy.tools.json_out()
457  def create_comparison(self):
458  """
459  Triggers the start of a now comparison between the revisions supplied
460  in revision_list
461  """
462  rev_list = cherrypy.request.json["revision_list"]
463  logging.debug("Creating plots for revisions: " + str(rev_list))
464  progress_key = start_plotting_request(
465  rev_list,
467  )
468  return {"progress_key": progress_key}
469 
470  @cherrypy.expose
471  def index(self):
472  """
473  forward to the static landing page if
474  the default url is used (like http://localhost:8080/)
475  """
476  raise cherrypy.HTTPRedirect("/static/validation.html")
477 
478  @cherrypy.expose
479  def plots(self, *args):
480  """
481  Serve file from the html/plot directory.
482  :param args: For the request /plots/a/b/c, these will be the strings
483  "a", "b", "c"
484  """
485 
486  warn_wrong_directory()
487 
488  if len(args) < 3:
489  raise cherrypy.HTTPError(404)
490 
491  tag_folder = os.path.relpath(
493  self.working_folderworking_folder, args[:-2]
494  ),
496  )
497  path = os.path.join(tag_folder, *args[-2:])
498  return cherrypy.lib.static.serve_file(path)
499 
500  @cherrypy.expose
501  @cherrypy.tools.json_in()
502  @cherrypy.tools.json_out()
504  """
505  Checks on the status of a comparison creation
506  """
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)
510  return status
511 
512  @cherrypy.expose
513  @cherrypy.tools.json_out()
514  def revisions(self, revision_label=None):
515  """
516  Return a combined json object with all revisions and
517  mark the newest one with the field most_recent=true
518  """
519 
520  # get list of available revision
521  rev_list = get_json_object_list(
523  validationpath.file_name_results_json,
524  )
525 
526  # always add the reference revision
527  combined_list = []
528  reference_revision = json.loads(
529  json_objects.dumps(json_objects.Revision(label="reference"))
530  )
531 
532  # load and combine
533  for r in rev_list:
534  full_path = os.path.join(
536  r,
537  validationpath.file_name_results_json,
538  )
539 
540  # update label, if dir has been moved
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)
545 
546  # Sorting
547 
548  # Order by categories (nightly, release, etc.) first, then by date
549  # A pure chronological order doesn't make sense, because we do not
550  # have a linear history ((pre)releases branch off) and for the builds
551  # the date corresponds to the build date, not to the date of the
552  # actual commit.
553  def sort_key(label: str):
554  if "-" not in label:
555  logging.warning(
556  f"Misformatted label encountered: '{label}' "
557  f"(doesn't seem to include date?)"
558  )
559  return label
560  category, datetag = label.split("-", maxsplit=1)
561  print(category, datetag)
562  # Will later reverse order to bring items in the same category
563  # in reverse chronological order, so the following list will have
564  # the items in reverse order as well:
565  order = ["release", "prerelease", "nightly"]
566  try:
567  index = order.index(category)
568  except ValueError:
569  index = 9
570  logging.warning(
571  f"Misformatted label encountered: '{label}' (doesn't seem "
572  f"to belong to any known category?)"
573  )
574  return f"{index}-{datetag}"
575 
576  combined_list.sort(key=lambda rev: sort_key(rev["label"]), reverse=True)
577 
578  # reference always on top
579  combined_list = [reference_revision] + combined_list
580 
581  # Set the most recent one ...
582  newest_date = None
583  newest_rev = None
584  for r in combined_list:
585  rdate_str = r["creation_date"]
586  if isinstance(rdate_str, str):
587  if len(rdate_str) > 0:
588  try:
589  rdate = time.strptime(rdate_str, "%Y-%m-%d %H:%M")
590  except ValueError:
591  # some old validation results might still contain
592  # seconds and therefore cannot properly be converted
593  rdate = None
594 
595  if rdate is None:
596  continue
597 
598  if newest_date is None:
599  newest_date = rdate
600  newest_rev = r
601  if rdate > newest_date:
602  newest_date = rdate
603  newest_rev = r
604 
605  for c in combined_list:
606  if c["most_recent"] is not None:
607  c["most_recent"] = False
608 
609  # if there are no revisions at all, this might also be just None
610  if newest_rev:
611  newest_rev["most_recent"] = True
612 
613  # topmost item must be dictionary for the ractive.os template to match
614  return {"revisions": combined_list}
615 
616  @cherrypy.expose
617  @cherrypy.tools.json_out()
618  def comparisons(self, comparison_label=None):
619  """
620  return the json file of the comparison results of one specific
621  comparison
622  """
623 
624  warn_wrong_directory()
625 
626  # todo: Make this independent of our working directory!
627  path = os.path.join(
628  os.path.relpath(
630  self.working_folderworking_folder, comparison_label.split(",")
631  ),
633  ),
634  "comparison.json",
635  )
636 
637  # check if this comparison actually exists
638  if not os.path.isfile(path):
639  raise cherrypy.HTTPError(
640  404, f"Json Comparison file {path} does not exist"
641  )
642 
643  return deliver_json(path)
644 
645  @cherrypy.expose
646  @cherrypy.tools.json_out()
647  def system_info(self):
648  """
649  Returns:
650  JSON file containing git versions and time of last restart
651  """
652 
653  warn_wrong_directory()
654 
655  # note: for some reason %Z doesn't work like this, so we use
656  # time.tzname for the time zone.
657  return {
658  "last_restart": self.last_restartlast_restart.strftime("%-d %b %H:%M ")
659  + time.tzname[1],
660  "version_restart": self.versionversion,
661  "version_current": validationfunctions.get_compact_git_hash(
662  os.environ["BELLE2_LOCAL_DIR"]
663  ),
664  }
665 
666  @cherrypy.expose
667  def retrieve_file_metadata(self, filename):
668  """
669  Returns:
670  Metadata(str) of the file
671  """
672  cherrypy.response.headers['Content-Type'] = 'text/plain'
673  metadata = validationfunctions.get_file_metadata(filename)
674  return metadata
675 
676  @cherrypy.expose
677  @cherrypy.tools.json_in()
678  @cherrypy.tools.json_out()
679  def create_issue(self, title, description):
680  """
681  Call the functions to create the issue and redirect
682  to the created Gitlab issue page.
683  """
684 
685  default_section = self.gitlab_configgitlab_config['global']['default']
686  project_id = self.gitlab_configgitlab_config[default_section]['project_id']
687  # Create issue in the Gitlab project and save it
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(
694  plot_title,
695  self.revision_labelrevision_label
696  )
697  issue_id = create_gitlab_issue(
698  title, description, uploaded_file, project
699  )
700  project.save()
701 
702  # Update JSON with created issue id
703  comparison_json_path = os.path.join(
705  self.plot_pathplot_path.split("/")[-3],
706  "comparison.json",
707  )
708 
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"):
714  if (
715  plot["png_filename"]
716  == self.plot_pathplot_path.split("/")[-1]
717  ):
718  plot["issue"].append(issue_id)
719  break
720 
721  with open(comparison_json_path, "w") as jsonFile:
722  json.dump(comparison_json, jsonFile, indent=4)
723 
724  issue_url = self.gitlab_configgitlab_config[default_section]['project_url'] \
725  + "/-/issues/" \
726  + str(issue_id)
727  raise cherrypy.HTTPRedirect(
728  issue_url
729  )
730 
731  @cherrypy.expose
732  def issue(self, file_path, rev_label):
733  """
734  Return a template issue creation interface
735  for the user to add title and description.
736  """
737  self.plot_pathplot_path = os.path.join(
738  validationpath.get_html_folder(self.working_folderworking_folder), file_path
739  )
740 
741  self.revision_labelrevision_label = rev_label
742 
743  if not self.gitlab_objectgitlab_object:
744  return "ERROR: Gitlab integration not set up, verify config file."
745 
746  raise cherrypy.HTTPRedirect("/static/validation_issue.html")
747 
748  @cherrypy.expose
749  def issue_redirect(self, iid):
750  """
751  Redirect to the Gitlab issue page.
752  """
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."
756 
757  issue_url = self.gitlab_configgitlab_config[default_section]['project_url'] \
758  + "/-/issues/" \
759  + str(iid)
760  raise cherrypy.HTTPRedirect(
761  issue_url
762  )
763 
764  @cherrypy.expose
765  def update_issue(self, id, file_path, rev_label):
766  """
767  Update existing issue in Gitlab with current result plot
768  and redirect to the updated Gitlab issue page.
769  """
770 
771  if not self.gitlab_objectgitlab_object:
772  return "ERROR: Gitlab integration not set up, verify config file."
773 
774  plot_path = os.path.join(
775  validationpath.get_html_folder(self.working_folderworking_folder), file_path
776  )
777 
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)
782  update_gitlab_issue(
783  id, uploaded_file, project, plot_path, rev_label
784  )
785  project.save()
786 
787  issue_url = self.gitlab_configgitlab_config[default_section]['project_url'] \
788  + "/-/issues/" \
789  + str(id)
790 
791  raise cherrypy.HTTPRedirect(
792  issue_url
793  )
794 
795 
796 def setup_gzip_compression(path, cherry_config):
797  """
798  enable GZip compression for all text-based content the
799  web-server will deliver
800  """
801 
802  cherry_config[path].update(
803  {
804  "tools.gzip.on": True,
805  "tools.gzip.mime_types": [
806  "text/html",
807  "text/plain",
808  "text/css",
809  "application/javascript",
810  "application/json",
811  ],
812  }
813  )
814 
815 
816 def get_argument_parser():
817  """
818  Prepare a parser for all the known command line arguments
819  """
820 
821  # Set up the command line parser
822  parser = argparse.ArgumentParser()
823 
824  # Define the accepted command line flags and read them in
825  parser.add_argument(
826  "-ip",
827  "--ip",
828  help="The IP address on which the"
829  "server starts. Default is '127.0.0.1'.",
830  type=str,
831  default="127.0.0.1",
832  )
833  parser.add_argument(
834  "-p",
835  "--port",
836  help="The port number on which"
837  " the server starts. Default is '8000'.",
838  type=str,
839  default=8000,
840  )
841  parser.add_argument(
842  "-v",
843  "--view",
844  help="Open validation website" " in the system's default browser.",
845  action="store_true",
846  )
847  parser.add_argument(
848  "--production",
849  help="Run in production environment: "
850  "no log/error output via website and no auto-reload",
851  action="store_true",
852  )
853 
854  return parser
855 
856 
857 def parse_cmd_line_arguments():
858  """!
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]
864  """
865  parser = get_argument_parser()
866  # Return the parsed arguments!
867  return parser.parse_args()
868 
869 
870 def run_server(
871  ip="127.0.0.1",
872  port=8000,
873  parse_command_line=False,
874  open_site=False,
875  dry_run=False,
876 ):
877 
878  # Setup options for logging
879  logging.basicConfig(
880  level=logging.DEBUG,
881  format="%(asctime)s %(levelname)-8s %(message)s",
882  datefmt="%H:%M:%S",
883  )
884 
885  basepath = validationpath.get_basepath()
886  cwd_folder = os.getcwd()
887 
888  # Only execute the program if a basf2 release is set up!
889  if (
890  os.environ.get("BELLE2_RELEASE_DIR", None) is None
891  and os.environ.get("BELLE2_LOCAL_DIR", None) is None
892  ):
893  sys.exit("Error: No basf2 release set up!")
894 
895  cherry_config = dict()
896  # just empty, will be filled below
897  cherry_config["/"] = {}
898  # will ensure also the json requests are gzipped
899  setup_gzip_compression("/", cherry_config)
900 
901  # check if static files are provided via central release
902  static_folder_list = ["validation", "html_static"]
903  static_folder = None
904 
905  if basepath["central"] is not None:
906  static_folder_central = os.path.join(
907  basepath["central"], *static_folder_list
908  )
909  if os.path.isdir(static_folder_central):
910  static_folder = static_folder_central
911 
912  # check if there is also a collection of static files in the local release
913  # this overwrites the usage of the central release
914  if basepath["local"] is not None:
915  static_folder_local = os.path.join(
916  basepath["local"], *static_folder_list
917  )
918  if os.path.isdir(static_folder_local):
919  static_folder = static_folder_local
920 
921  if static_folder is None:
922  sys.exit(
923  "Either BELLE2_RELEASE_DIR or BELLE2_LOCAL_DIR has to bet "
924  "to provide static HTML content. Did you run b2setup ?"
925  )
926 
927  # join the paths of the various result folders
928  results_folder = validationpath.get_results_folder(cwd_folder)
929  comparison_folder = validationpath.get_html_plots_folder(cwd_folder)
930 
931  logging.info(f"Serving static content from {static_folder}")
932  logging.info(f"Serving result content and plots from {cwd_folder}")
933 
934  # check if the results folder exists and has at least one folder
935  if not os.path.isdir(results_folder):
936  sys.exit(
937  "Result folder {} does not exist, run validate_basf2 first "
938  "to create validation output".format(results_folder)
939  )
940 
941  results_count = sum(
942  [
943  os.path.isdir(os.path.join(results_folder, f))
944  for f in os.listdir(results_folder)
945  ]
946  )
947  if results_count == 0:
948  sys.exit(
949  f"Result folder {results_folder} contains no folders, run "
950  f"validate_basf2 first to create validation output"
951  )
952 
953  # Go to the html directory
954  if not os.path.exists("html"):
955  os.mkdir("html")
956  os.chdir("html")
957 
958  if not os.path.exists("plots"):
959  os.mkdir("plots")
960 
961  # export js, css and html templates
962  cherry_config["/static"] = {
963  "tools.staticdir.on": True,
964  # only serve js, css, html and png files
965  "tools.staticdir.match": r"^.*\.(js|css|html|png|js.map)$",
966  "tools.staticdir.dir": static_folder,
967  }
968  setup_gzip_compression("/static", cherry_config)
969 
970  # export generated plots
971  cherry_config["/plots"] = {
972  "tools.staticdir.on": True,
973  # only serve json and png files
974  "tools.staticdir.match": r"^.*\.(png|json|pdf)$",
975  "tools.staticdir.dir": comparison_folder,
976  }
977  setup_gzip_compression("/plots", cherry_config)
978 
979  # export generated results and raw root files
980  cherry_config["/results"] = {
981  "tools.staticdir.on": True,
982  "tools.staticdir.dir": results_folder,
983  # only serve root files
984  "tools.staticdir.match": r"^.*\.(log|root)$",
985  # server the log files as plain text files, and make sure to use
986  # utf-8 encoding. Firefox might decide different, if the files
987  # are located on a .jp domain and use Shift_JIS
988  "tools.staticdir.content_types": {
989  "log": "text/plain; charset=utf-8",
990  "root": "application/octet-stream",
991  },
992  }
993 
994  setup_gzip_compression("/results", cherry_config)
995 
996  # Define the server address and port
997  # only if we got some specific
998  production_env = False
999  if parse_command_line:
1000  # Parse command line arguments
1001  cmd_arguments = parse_cmd_line_arguments()
1002 
1003  ip = cmd_arguments.ip
1004  port = int(cmd_arguments.port)
1005  open_site = cmd_arguments.view
1006  production_env = cmd_arguments.production
1007 
1008  cherrypy.config.update(
1009  {
1010  "server.socket_host": ip,
1011  "server.socket_port": port,
1012  }
1013  )
1014  if production_env:
1015  cherrypy.config.update({"environment": "production"})
1016 
1017  logging.info(f"Server: Starting HTTP server on {ip}:{port}")
1018 
1019  if open_site:
1020  webbrowser.open("http://" + ip + ":" + str(port))
1021 
1022  config_path = os.path.join(static_folder, '../config/gl.cfg')
1023 
1024  if not dry_run:
1025  # gitlab toggle
1026  gitlab_object = None
1027  gitlab_config = None
1028  if not os.path.exists(config_path):
1029  logging.warning(
1030  "ERROR: Expected to find config folder with Gitlab config,"
1031  f" but {config_path} doesn't exist. "
1032  "Gitlab features will not work."
1033  )
1034  else:
1035  gitlab_config = get_gitlab_config(config_path)
1036  gitlab_object = create_gitlab_object(config_path)
1037  if gitlab_object:
1038  update_linked_issues(gitlab_object, cwd_folder)
1039 
1040  cherrypy.quickstart(
1042  working_folder=cwd_folder,
1043  gitlab_object=gitlab_object,
1044  gitlab_config=gitlab_config,
1045  ),
1046  "/",
1047  cherry_config,
1048  )
1049 
1050 
1051 if __name__ == "__main__":
1052  run_server()
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)
def issue(self, file_path, rev_label)
revision_label
placeholder variable for revision label
def dumps(obj)
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)