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

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

26 

27import importlib 

28 

29from wuttjamaican.app import GenericHandler 

30 

31 

32class Report: 

33 """ 

34 Base class for all :term:`reports <report>`. 

35 

36 .. attribute:: report_key 

37 

38 Each report must define a unique key, to identify it. 

39 

40 .. attribute:: report_title 

41 

42 This is the common display title for the report. 

43 """ 

44 report_title = "Untitled Report" 

45 

46 def __init__(self, config): 

47 self.config = config 

48 self.app = config.get_app() 

49 

50 def add_params(self, schema): 

51 """ 

52 Add field nodes to the given schema, defining all 

53 :term:`report params`. 

54 

55 :param schema: :class:`~colander:colander.Schema` instance. 

56 

57 The schema is from Colander so nodes must be compatible with 

58 that; for instance:: 

59 

60 import colander 

61 

62 def add_params(self, schema): 

63 

64 schema.add(colander.SchemaNode( 

65 colander.Date(), 

66 name='start_date')) 

67 

68 schema.add(colander.SchemaNode( 

69 colander.Date(), 

70 name='end_date')) 

71 """ 

72 

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. 

77 

78 Each entry can be a simple column name, or else a dict with 

79 other options, e.g.:: 

80 

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 ] 

91 

92 :returns: List of column definitions as described above. 

93 

94 The last entry shown above has all options currently 

95 supported; here we explain those: 

96 

97 * ``name`` - True name for the column. 

98 

99 * ``label`` - Display label for the column. If not specified, 

100 one is derived from the ``name``. 

101 

102 * ``numeric`` - Boolean indicating the column data is numeric, 

103 so should be right-aligned. 

104 

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 

110 

111 def make_data(self, params, progress=None): 

112 """ 

113 This must "run" the report and return the final data. 

114 

115 Note that this should *not* (usually) write the data to file, 

116 its purpose is just to obtain the data. 

117 

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. 

121 

122 There is no default logic here; subclass must define. 

123 

124 :param params: Dict of :term:`report params`. 

125 

126 :param progress: Optional progress indicator factory. 

127 

128 :returns: Data dict, or list of rows. 

129 """ 

130 raise NotImplementedError 

131 

132 

133class ReportHandler(GenericHandler): 

134 """ 

135 Base class and default implementation for the :term:`report 

136 handler`. 

137 """ 

138 

139 def get_report_modules(self): 

140 """ 

141 Returns a list of all known :term:`report modules <report 

142 module>`. 

143 

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) 

158 

159 return self._report_modules 

160 

161 def get_reports(self): 

162 """ 

163 Returns a dict of all known :term:`reports <report>`, keyed by 

164 :term:`report key`. 

165 

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 

178 

179 return self._reports 

180 

181 def get_report(self, key, instance=True): 

182 """ 

183 Fetch the :term:`report` class or instance for given key. 

184 

185 :param key: Identifying :term:`report key`. 

186 

187 :param instance: Whether to return the class, or an instance. 

188 Default is ``True`` which means return the instance. 

189 

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 

199 

200 def make_report_data(self, report, params=None, progress=None, **kwargs): 

201 """ 

202 Run the given report and return the output data. 

203 

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:: 

207 

208 { 

209 'output_title': "My Report", 

210 'data': ..., 

211 } 

212 

213 However that is the *minimum*; the dict may have other keys as 

214 well. 

215 

216 :param report: :class:`Report` instance to run. 

217 

218 :param params: Dict of :term:`report params`. 

219 

220 :param progress: Optional progress indicator factory. 

221 

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