Coverage for functions \ flipdare \ task \ report \ core \ time_series_report.py: 78%
51 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python
2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
14from typing import Literal, override
15from collections.abc import Callable
16from flipdare.app_types import ReportListType
17from flipdare.task.report.core._admin_report import AdminReport
18from flipdare.backend.app_logger import AppLogger
19from flipdare.analysis.data._time_series_protocol import TimeSeriesProtocol
20from flipdare.analysis.plot.time_series_plotter import TimeSeriesPlotter
21from flipdare.app_log import LOG
22from flipdare.constants import IS_DEBUG
23from flipdare.mailer.core.email_composer import EmailComposer
24from flipdare.mailer.email_image import EmailImage
25from flipdare.mailer.admin_mailer import AdminMailer
26from flipdare.generated.shared.backend.app_job_type import AppJobType
28type TimeSeriesReportType = Literal[
29 AppJobType.REPORT_LOG_STATS,
30 AppJobType.REPORT_ERROR_STATS,
31 AppJobType.REPORT_JOB_TYPE_STATS,
32 AppJobType.REPORT_PAYMENT_STATS,
33]
36class TimeSeriesReport(AdminReport):
38 __slots__ = ("_plot_fn", "_query_fn")
40 def __init__(
41 self,
42 job_type: TimeSeriesReportType,
43 query_fn: Callable[..., TimeSeriesProtocol],
44 plot_fn: Callable[[TimeSeriesProtocol], TimeSeriesPlotter],
45 app_logger: AppLogger,
46 mailer: AdminMailer,
47 ) -> None:
48 super().__init__(
49 job_type=job_type,
50 app_logger=app_logger,
51 mailer=mailer,
52 )
53 self._query_fn = query_fn
54 self._plot_fn = plot_fn
56 @override
57 def compose(self) -> EmailComposer | None:
58 label = self.job_type.value
60 try:
61 stats = self._query_fn()
62 if IS_DEBUG:
63 LOG().debug(f"Report {label} generated {stats.count} results")
64 except Exception as ex:
65 msg = f"Failed to generate report data for {label}: {ex}"
66 self.add_error(msg)
67 return None
69 image_builder = self._plot_fn(stats)
70 plots: list[EmailImage] = []
71 try:
72 result = image_builder.create()
73 if not result.is_error:
74 plots = result.plots
75 else:
76 error_str = "; ".join(result.errors)
77 msg = f"Report {label} encountered errors during image creation: {error_str}"
78 self.add_error(msg)
80 except Exception as ex:
81 msg = f"Failed to create images for report {label}: {ex}"
82 self.add_error(msg)
84 return self._compose_result(
85 data=stats.table_data(),
86 headers=stats.headers,
87 plots=plots,
88 )
90 def _compose_result(
91 self,
92 data: ReportListType,
93 headers: list[str],
94 plots: list[EmailImage] | None = None,
95 ) -> EmailComposer | None:
96 try:
97 return (
98 EmailComposer.table(
99 data=data,
100 headers=headers,
101 priority=self.priority,
102 )
103 if plots is None
104 else EmailComposer.gallery(
105 data=data,
106 headers=headers,
107 priority=self.priority,
108 images=plots,
109 )
110 )
111 except Exception as ex:
112 msg = f"Failed to build HTML report: {ex}"
113 self.add_error(msg)
114 return None