Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/reports.py: 100%
49 statements
« prev ^ index » next coverage.py v7.3.2, created at 2025-01-11 22:02 -0600
« prev ^ index » next coverage.py v7.3.2, created at 2025-01-11 22:02 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# WuttJamaican -- Base package for Wutta Framework
5# Copyright © 2023-2024 Lance Edgar
6#
7# This file is part of Wutta Framework.
8#
9# Wutta Framework is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the Free
11# Software Foundation, either version 3 of the License, or (at your option) any
12# later version.
13#
14# Wutta Framework is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17# more details.
18#
19# You should have received a copy of the GNU General Public License along with
20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Report Utilities
25"""
27import importlib
29from wuttjamaican.app import GenericHandler
32class Report:
33 """
34 Base class for all :term:`reports <report>`.
36 .. attribute:: report_key
38 Each report must define a unique key, to identify it.
40 .. attribute:: report_title
42 This is the common display title for the report.
43 """
44 report_title = "Untitled Report"
46 def __init__(self, config):
47 self.config = config
48 self.app = config.get_app()
50 def add_params(self, schema):
51 """
52 Add field nodes to the given schema, defining all
53 :term:`report params`.
55 :param schema: :class:`~colander:colander.Schema` instance.
57 The schema is from Colander so nodes must be compatible with
58 that; for instance::
60 import colander
62 def add_params(self, schema):
64 schema.add(colander.SchemaNode(
65 colander.Date(),
66 name='start_date'))
68 schema.add(colander.SchemaNode(
69 colander.Date(),
70 name='end_date'))
71 """
73 def get_output_columns(self):
74 """
75 This should return a list of column definitions to be used
76 when displaying or persisting the data output.
78 Each entry can be a simple column name, or else a dict with
79 other options, e.g.::
81 def get_output_columns(self):
82 return [
83 'foo',
84 {'name': 'bar',
85 'label': "BAR"},
86 {'name': 'sales',
87 'label': "Total Sales",
88 'numeric': True,
89 'formatter': self.app.render_currency},
90 ]
92 :returns: List of column definitions as described above.
94 The last entry shown above has all options currently
95 supported; here we explain those:
97 * ``name`` - True name for the column.
99 * ``label`` - Display label for the column. If not specified,
100 one is derived from the ``name``.
102 * ``numeric`` - Boolean indicating the column data is numeric,
103 so should be right-aligned.
105 * ``formatter`` - Custom formatter / value rendering callable
106 for the column. If set, this will be called with just one
107 arg (the value) for each data row.
108 """
109 raise NotImplementedError
111 def make_data(self, params, progress=None):
112 """
113 This must "run" the report and return the final data.
115 Note that this should *not* (usually) write the data to file,
116 its purpose is just to obtain the data.
118 The return value should usually be a dict, with no particular
119 structure required beyond that. However it also can be a list
120 of data rows.
122 There is no default logic here; subclass must define.
124 :param params: Dict of :term:`report params`.
126 :param progress: Optional progress indicator factory.
128 :returns: Data dict, or list of rows.
129 """
130 raise NotImplementedError
133class ReportHandler(GenericHandler):
134 """
135 Base class and default implementation for the :term:`report
136 handler`.
137 """
139 def get_report_modules(self):
140 """
141 Returns a list of all known :term:`report modules <report
142 module>`.
144 This will discover all report modules exposed by the
145 :term:`app`, and/or its :term:`providers <provider>`.
146 """
147 if not hasattr(self, '_report_modules'):
148 self._report_modules = []
149 for provider in self.app.providers.values():
150 if hasattr(provider, 'report_modules'):
151 modules = provider.report_modules
152 if modules:
153 if isinstance(modules, str):
154 modules = [modules]
155 for module in modules:
156 module = importlib.import_module(module)
157 self._report_modules.append(module)
159 return self._report_modules
161 def get_reports(self):
162 """
163 Returns a dict of all known :term:`reports <report>`, keyed by
164 :term:`report key`.
166 This calls :meth:`get_report_modules()` and for each module,
167 it discovers all the reports it contains.
168 """
169 if not hasattr(self, '_reports'):
170 self._reports = {}
171 for module in self.get_report_modules():
172 for name in dir(module):
173 obj = getattr(module, name)
174 if (isinstance(obj, type)
175 and obj is not Report
176 and issubclass(obj, Report)):
177 self._reports[obj.report_key] = obj
179 return self._reports
181 def get_report(self, key, instance=True):
182 """
183 Fetch the :term:`report` class or instance for given key.
185 :param key: Identifying :term:`report key`.
187 :param instance: Whether to return the class, or an instance.
188 Default is ``True`` which means return the instance.
190 :returns: :class:`Report` class or instance, or ``None`` if
191 the report could not be found.
192 """
193 reports = self.get_reports()
194 if key in reports:
195 report = reports[key]
196 if instance:
197 report = report(self.config)
198 return report
200 def make_report_data(self, report, params=None, progress=None, **kwargs):
201 """
202 Run the given report and return the output data.
204 This calls :meth:`Report.make_data()` on the report, and
205 tweaks the output as needed for consistency. The return value
206 should resemble this structure::
208 {
209 'output_title': "My Report",
210 'data': ...,
211 }
213 However that is the *minimum*; the dict may have other keys as
214 well.
216 :param report: :class:`Report` instance to run.
218 :param params: Dict of :term:`report params`.
220 :param progress: Optional progress indicator factory.
222 :returns: Data dict with structure shown above.
223 """
224 data = report.make_data(params or {}, progress=progress, **kwargs)
225 if not isinstance(data, dict):
226 data = {'data': data}
227 data.setdefault('output_title', report.report_title)
228 return data