Coverage for functions \ flipdare \ task \ command \ _base_command.py: 39%
93 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#
13from typing import Any
14from collections.abc import Callable, Mapping
15from flipdare.backend.app_logger import AppLogger
16from flipdare.app_log import LOG
17from flipdare.constants import IS_DEBUG
18from flipdare.result.output_result import OutputResult
19from flipdare.mailer.admin_mailer import AdminMailer
20from flipdare.mailer._jinja_email_template import JinjaEmailTemplate
21from flipdare.firestore.backend.exchange_rate_db import ExchangeRateDb
22from flipdare.generated.shared.app_error_code import AppErrorCode
23from flipdare.generated.shared.backend.app_job_type import AppJobType
24from flipdare.generated.shared.backend.app_report_priority import AppReportPriority
25from flipdare.util.time_util import TimeUtil
28class BaseCommand[TSchema: Mapping[str, Any]]:
29 def __init__(
30 self,
31 job_type: AppJobType,
32 schema_class: type[TSchema],
33 command_callback: Callable[[], None | OutputResult],
34 email_callback: Callable[[str, Exception | None], JinjaEmailTemplate[Any]],
35 app_logger: AppLogger | None = None,
36 mailer: AdminMailer | None = None,
37 exchange_rate_db: ExchangeRateDb | None = None,
38 ) -> None:
39 self._job_type = job_type
40 self._schema_class = schema_class
41 self._command_callback = command_callback
42 self._email_callback = email_callback
43 self._app_logger = app_logger
44 self._mailer = mailer
45 self._exchange_rate_db = exchange_rate_db
47 def run_command(self) -> OutputResult:
48 job_type = self.job_type
49 debug_str = f"Command: {job_type.label} - {job_type.description}"
50 ex_error: Exception | None = None
52 start = TimeUtil.get_current_utc_dt()
54 try:
55 result = self._command_callback()
56 if isinstance(result, OutputResult):
57 message = result.message
58 else:
59 message = f"Successfully ran {self.command_name} command."
60 except Exception as ex:
61 message = f"Failed to run {self.command_name} command."
62 ex_error = ex
64 template = self._email_callback(message, ex_error)
65 ok = self.send_template(email_template=template)
67 end = TimeUtil.get_current_utc_dt()
68 duration = TimeUtil.duration_in_seconds(start, end)
69 if ok and ex_error is None:
70 if IS_DEBUG:
71 LOG().debug(f"Successfully ran command: {debug_str}")
73 return OutputResult.ok(job_type=job_type, message=message, duration=duration)
75 # handle error
76 LOG().error(f"Error running command: {debug_str} - {message}")
77 return OutputResult.error(
78 error_code=AppErrorCode.COMMAND,
79 duration=duration,
80 job_type=job_type,
81 message=message,
82 )
84 @property
85 def app_logger(self) -> AppLogger:
86 from flipdare.services import get_app_logger
88 if self._app_logger is None:
89 self._app_logger = get_app_logger()
90 return self._app_logger
92 @property
93 def mailer(self) -> AdminMailer:
94 from flipdare.services import get_admin_mailer
96 if self._mailer is None:
97 self._mailer = get_admin_mailer()
98 return self._mailer
100 @property
101 def exchange_rate_db(self) -> ExchangeRateDb:
102 from flipdare.services import get_exchange_rate_db
104 if self._exchange_rate_db is None:
105 self._exchange_rate_db = get_exchange_rate_db()
106 return self._exchange_rate_db
108 @property
109 def keys(self) -> list[str]:
110 return list(self._schema_class.__annotations__.keys())
112 @property
113 def command_name(self) -> str:
114 return self.job_type.label
116 @property
117 def job_type(self) -> AppJobType:
118 return self._job_type
120 @property
121 def priority(self) -> AppReportPriority:
122 return self._job_type.priority
124 @property
125 def command_description(self) -> str:
126 return self.job_type.description
128 @property
129 def _debug_label(self) -> str:
130 return f"{self.command_name}/{self.command_description}\n"
132 def send_template(self, email_template: JinjaEmailTemplate[Any]) -> bool:
133 LOG().info(f"Sending {self.command_name} report email to admins.")
134 try:
135 self.mailer.send(email_template=email_template)
136 return True
137 except Exception as ex:
138 cause = f"Failed to send {self.command_name} report email: {ex}"
139 self.log_error(cause)
140 return False
142 def log_error(self, cause: str) -> None:
143 LOG().error(f"{self.command_name} Report Error: {cause}")
144 self.app_logger.system_error(
145 job_type=self.job_type,
146 error_code=AppErrorCode.SERVER_REPORT,
147 message=cause,
148 notify_admin=True,
149 )