Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/reports.py: 100%
37 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 13:14 -0500
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 13:14 -0500
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# WuttJamaican -- Base package for Wutta Framework
5# Copyright © 2023-2025 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"""
27from wuttjamaican.app import GenericHandler
30class Report:
31 """
32 Base class for all :term:`reports <report>`.
34 .. attribute:: report_key
36 Each report must define a unique key, to identify it.
38 .. attribute:: report_title
40 This is the common display title for the report.
41 """
43 report_title = "Untitled Report"
45 def __init__(self, config):
46 self.config = config
47 self.app = config.get_app()
49 def add_params(self, schema):
50 """
51 Add field nodes to the given schema, defining all
52 :term:`report params`.
54 :param schema: :class:`~colander:colander.Schema` instance.
56 The schema is from Colander so nodes must be compatible with
57 that; for instance::
59 import colander
61 def add_params(self, schema):
63 schema.add(colander.SchemaNode(
64 colander.Date(),
65 name='start_date'))
67 schema.add(colander.SchemaNode(
68 colander.Date(),
69 name='end_date'))
70 """
72 def get_output_columns(self):
73 """
74 This should return a list of column definitions to be used
75 when displaying or persisting the data output.
77 Each entry can be a simple column name, or else a dict with
78 other options, e.g.::
80 def get_output_columns(self):
81 return [
82 'foo',
83 {'name': 'bar',
84 'label': "BAR"},
85 {'name': 'sales',
86 'label': "Total Sales",
87 'numeric': True,
88 'formatter': self.app.render_currency},
89 ]
91 :returns: List of column definitions as described above.
93 The last entry shown above has all options currently
94 supported; here we explain those:
96 * ``name`` - True name for the column.
98 * ``label`` - Display label for the column. If not specified,
99 one is derived from the ``name``.
101 * ``numeric`` - Boolean indicating the column data is numeric,
102 so should be right-aligned.
104 * ``formatter`` - Custom formatter / value rendering callable
105 for the column. If set, this will be called with just one
106 arg (the value) for each data row.
107 """
108 raise NotImplementedError
110 def make_data(self, params, progress=None):
111 """
112 This must "run" the report and return the final data.
114 Note that this should *not* (usually) write the data to file,
115 its purpose is just to obtain the data.
117 The return value should usually be a dict, with no particular
118 structure required beyond that. However it also can be a list
119 of data rows.
121 There is no default logic here; subclass must define.
123 :param params: Dict of :term:`report params`.
125 :param progress: Optional progress indicator factory.
127 :returns: Data dict, or list of rows.
128 """
129 raise NotImplementedError
132class ReportHandler(GenericHandler):
133 """
134 Base class and default implementation for the :term:`report
135 handler`.
136 """
138 def get_report_modules(self):
139 """
140 Returns a list of all known :term:`report modules <report
141 module>`.
143 This will discover all report modules exposed by the
144 :term:`app`, and/or its :term:`providers <provider>`.
146 Calls
147 :meth:`~wuttjamaican.app.GenericHandler.get_provider_modules()`
148 under the hood, for ``report`` module type.
149 """
150 return self.get_provider_modules("report")
152 def get_reports(self):
153 """
154 Returns a dict of all known :term:`reports <report>`, keyed by
155 :term:`report key`.
157 This calls :meth:`get_report_modules()` and for each module,
158 it discovers all the reports it contains.
159 """
160 if "reports" not in self.classes:
161 self.classes["reports"] = {}
162 for module in self.get_report_modules():
163 for name in dir(module):
164 obj = getattr(module, name)
165 if (
166 isinstance(obj, type)
167 and obj is not Report
168 and issubclass(obj, Report)
169 ):
170 self.classes["reports"][obj.report_key] = obj
172 return self.classes["reports"]
174 def get_report(self, key, instance=True):
175 """
176 Fetch the :term:`report` class or instance for given key.
178 :param key: Identifying :term:`report key`.
180 :param instance: Whether to return the class, or an instance.
181 Default is ``True`` which means return the instance.
183 :returns: :class:`Report` class or instance, or ``None`` if
184 the report could not be found.
185 """
186 reports = self.get_reports()
187 if key in reports:
188 report = reports[key]
189 if instance:
190 report = report(self.config)
191 return report
192 return None
194 def make_report_data(self, report, params=None, progress=None, **kwargs):
195 """
196 Run the given report and return the output data.
198 This calls :meth:`Report.make_data()` on the report, and
199 tweaks the output as needed for consistency. The return value
200 should resemble this structure::
202 {
203 'output_title': "My Report",
204 'data': ...,
205 }
207 However that is the *minimum*; the dict may have other keys as
208 well.
210 :param report: :class:`Report` instance to run.
212 :param params: Dict of :term:`report params`.
214 :param progress: Optional progress indicator factory.
216 :returns: Data dict with structure shown above.
217 """
218 data = report.make_data(params or {}, progress=progress, **kwargs)
219 if not isinstance(data, dict):
220 data = {"data": data}
221 data.setdefault("output_title", report.report_title)
222 return data