Files
pmsco-public/pmsco/reports/base.py

230 lines
7.3 KiB
Python

"""
@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