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

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

26 

27from wuttjamaican.app import GenericHandler 

28 

29 

30class Report: 

31 """ 

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

33 

34 .. attribute:: report_key 

35 

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

37 

38 .. attribute:: report_title 

39 

40 This is the common display title for the report. 

41 """ 

42 

43 report_title = "Untitled Report" 

44 

45 def __init__(self, config): 

46 self.config = config 

47 self.app = config.get_app() 

48 

49 def add_params(self, schema): 

50 """ 

51 Add field nodes to the given schema, defining all 

52 :term:`report params`. 

53 

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

55 

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

57 that; for instance:: 

58 

59 import colander 

60 

61 def add_params(self, schema): 

62 

63 schema.add(colander.SchemaNode( 

64 colander.Date(), 

65 name='start_date')) 

66 

67 schema.add(colander.SchemaNode( 

68 colander.Date(), 

69 name='end_date')) 

70 """ 

71 

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. 

76 

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

78 other options, e.g.:: 

79 

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 ] 

90 

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

92 

93 The last entry shown above has all options currently 

94 supported; here we explain those: 

95 

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

97 

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

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

100 

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

102 so should be right-aligned. 

103 

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 

109 

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

111 """ 

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

113 

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

115 its purpose is just to obtain the data. 

116 

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. 

120 

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

122 

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

124 

125 :param progress: Optional progress indicator factory. 

126 

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

128 """ 

129 raise NotImplementedError 

130 

131 

132class ReportHandler(GenericHandler): 

133 """ 

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

135 handler`. 

136 """ 

137 

138 def get_report_modules(self): 

139 """ 

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

141 module>`. 

142 

143 This will discover all report modules exposed by the 

144 :term:`app`, and/or its :term:`providers <provider>`. 

145 

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

151 

152 def get_reports(self): 

153 """ 

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

155 :term:`report key`. 

156 

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 

171 

172 return self.classes["reports"] 

173 

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

175 """ 

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

177 

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

179 

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

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

182 

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 

193 

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

195 """ 

196 Run the given report and return the output data. 

197 

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

201 

202 { 

203 'output_title': "My Report", 

204 'data': ..., 

205 } 

206 

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

208 well. 

209 

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

211 

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

213 

214 :param progress: Optional progress indicator factory. 

215 

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