""" @package pmsco.reports.base base class of project reports @author Matthias Muntwiler, matthias.muntwiler@psi.ch @copyright (c) 2021 by Paul Scherrer Institut @n Licensed under the Apache License, Version 2.0 (the "License"); @n you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 """ import logging from pathlib import Path from string import Template import pmsco.config as config logger = logging.getLogger(__name__) class ProjectReport(config.ConfigurableObject): """ base class of project reports what do we need to know from project? - directories to resolve path names - database session factory - database job id - calculation id usage: 1. assign public attributes as necessary, leave defaults at None. 2. call validate() if necessary. 3. call set_database() if necessary. 4. load data into result_data by calling select_data() or by modifying result_data directly. 5. call create_report() implementations of reports should not need to access any project members directly! this class is under development """ ## @var _project # project object reference # ## @var _dba # database access (session factory) # ## @var _modes # compatible project modes # # set of modes in which the report can be used. # if an incompatible project is assigned, the enabled property is set to False. ## @var canvas # matplotlib canvas for graphical reports # # a FigureCanvas class reference of a matplotlib backend. # the default is matplotlib.backends.backend_agg.FigureCanvasAgg # which produces a bitmap file in PNG format. # some other options are # matplotlib.backends.backend_pdf.FigureCanvasPdf or # matplotlib.backends.backend_svg.FigureCanvasSVG. ## @var enabled # enable/disable the report # # the flag allows to temporarily enable or disable a report. # # the flag does not change the behaviour of the class. # it is up to the caller to respect it. # # the validate method can set the flag to False if the project is not compatible. ## @var report_dir # destination directory for report files # # this should be a Path or PathLike object. # by default, the project's directories['report'] entry is used. ## @var base_filename # base name of output files # # this value gets copied into the {base} placeholder of filename_format. # # by default, this is the stem of the output filename from the project settings. ## @var filename_format (str) # format of the output file name # # the format method of the string will be used to produce individual names. # possible fields are: # base, job_id, job_name, mode, model, gen, particle, param0, param1 # where base corresponds to the stem of the output files produced by the calculators. # # a file extension according to the file format is appended. ## @var title_format (str) # title of the plot # # the format method of the string will be used to produce individual titles. # possible fields are: # base, job_id, job_name, mode, model, gen, particle, param0, param1 ## @var trigger_levels (set of str) # events that may trigger creation of a report # # this attribute selects when a report is created. # the following values are currently recognized. # any other value disables the report. # further modes may be implemented in the future. # # - `model` - every time a new model has been calculated. # - `end` (default) - only once at the end of an optimization job. # # it is up to the calling code to respect this attribute. # it does not affect the behaviour of the class. def __init__(self): super().__init__() self._project = None self._dba = None self._modes = set() self.enabled = True self.trigger_levels = {'end'} self.canvas = None self.report_dir = None self.base_filename = None self.filename_format = "${base}" self.title_format = "" def get_session(self): """ get a new database session context handler @return: context handler which provides an sqlalchemy database session, e.g. a pmsco.database.access.LockedSession() object. """ return self._dba.session() def validate(self, project): """ validate the configuration (object properties). @param project: pmsco.project.Project object, or any object that contains equivalent directories and output_file attributes. @return: None """ self._project = project if self._project: if self.report_dir is None: self.report_dir = self._project.directories['report'] if self.base_filename is None: self.base_filename = self._project.output_file.name if self.enabled and self._project.mode not in self._modes: self.enabled = False logger.warning(f"project mode {self._project.mode} incompatible with {self.__class__.__name__}") if self.report_dir: Path(self.report_dir).mkdir(exist_ok=True) def set_database(self, database_access): """ preparation steps for the report. @param database_access: fully initialized pmsco.database.project.ProjectDatabase object which provides database sessions. @return: None """ self._dba = database_access def resolve_template(self, template, mapping): """ resolve placeholders in template string the function first tries to resolve using the project's resolve_path function and reverts to plain python Template strings if no project is set. @param template: template string using ${name}-style place holders. name must be declared in project.directories or mapping. non-string objects (e.g. Path) are converted to a string using the str function. @param mapping: dictionary of name-value pairs for placeholders. @return: resolved string """ try: r = self._project.directories.resolve_path(template, mapping) except AttributeError: if template: r = Template(str(template)).substitute(mapping) else: r = "" return r def select_data(self, jobs=-1, calcs=None): """ query data from the database this method must be implemented by the sub-class. @param jobs: filter by job. the argument can be a singleton or sequence of orm.Job objects or numeric id. if None, results from all jobs are loaded. if -1 (default), results from the most recent job (by datetime field) are loaded. @param calcs: filter by result. the argument can be a singleton or sequence of CalcID objects. if None (default), all results are loaded. if -1, the most recent result is loaded. @return: None """ pass def create_report(self): """ generate or update the report from stored data. this method must be implemented by the sub-class. @return: None """ pass