From 8099e5b8cd6c4ab80c52d9f99ee4d6b77d154007 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 15 Mar 2025 15:30:03 -0400 Subject: [PATCH] feat: `HassLogger` --- src/home_assistant/logger.rs | 180 +++++++++++++++++++++++++++++++++++ src/home_assistant/mod.rs | 1 + 2 files changed, 181 insertions(+) create mode 100644 src/home_assistant/logger.rs diff --git a/src/home_assistant/logger.rs b/src/home_assistant/logger.rs new file mode 100644 index 0000000..7f0f190 --- /dev/null +++ b/src/home_assistant/logger.rs @@ -0,0 +1,180 @@ +use pyo3::{prelude::*, types::PyTuple}; + +use crate::{ + arbitrary::{arbitrary::Arbitrary, map::Map}, + python_utils::{detach, validate_type_by_name}, +}; + +#[derive(Debug)] +pub struct HassLogger(Py); + +impl<'source> FromPyObject<'source> for HassLogger { + fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult { + // region: Validation + validate_type_by_name(ob, "HassLogger")?; + // endregion: Validation + + Ok(Self(detach(ob))) + } +} + +#[derive(Debug, Clone, IntoPyObject)] +pub struct LogData { + /// If exc_info does not evaluate as false, it causes exception information to be added to the logging message. + /// If an exception tuple (in the format returned by sys.exc_info()) or an exception instance is provided, it is used; + /// otherwise, sys.exc_info() is called to get the exception information. + exc_info: Option, + + /// If true, stack information is added to the logging message, including the actual logging call. + /// Note that this is not the same stack information as that displayed through specifying exc_info: + /// The former is stack frames from the bottom of the stack up to the logging call in the current thread, + /// whereas the latter is information about stack frames which have been unwound, + /// following an exception, while searching for exception handlers. + /// + /// You can specify stack_info independently of exc_info, + /// e.g. to just show how you got to a certain point in your code, even when no exceptions were raised. + /// The stack frames are printed following a header line which says: + /// + /// Stack (most recent call last): + /// + /// This mimics the `Traceback (most recent call last):` which is used when displaying exception frames. + stack_info: bool, + + /// If greater than 1, the corresponding number of stack frames are skipped + /// when computing the line number and function name set in the LogRecord created for the logging event. + /// This can be used in logging helpers so that the function name, filename and line number recorded + /// are not the information for the helper function/method, but rather its caller. + stacklevel: u16, + + /// This can be used to pass a dictionary which is used to populate the __dict__ of the LogRecord + /// created for the logging event with user-defined attributes. + /// These custom attributes can then be used as you like. + /// For example, they could be incorporated into logged messages. + extra: Map, +} + +impl HassLogger { + pub fn new(py: Python<'_>, name: &str) -> PyResult { + let logging = py.import("logging")?; + let logger = logging.call_method1("getLogger", (name,))?; + + Ok(logger.extract()?) + } + + pub fn debug<'py, ExcInfo: IntoPyObject<'py>>( + &self, + py: Python<'py>, + msg: &str, + args: Vec, + log_data: Option>, + ) -> PyResult<()> { + let mut all_args = vec![msg.into_pyobject(py)?.into_any()]; + for arg in args { + let arg = arg.into_pyobject(py)?; + all_args.push(arg); + } + let all_args = PyTuple::new(py, all_args)?; + + let kwargs = log_data + .map(|log_data| log_data.into_pyobject(py)) + .transpose()?; + + self.0.call_method(py, "debug", all_args, kwargs.as_ref())?; + + Ok(()) + } + + pub fn info<'py, ExcInfo: IntoPyObject<'py>>( + &self, + py: Python<'py>, + msg: &str, + args: Vec, + log_data: Option>, + ) -> PyResult<()> { + let mut all_args = vec![msg.into_pyobject(py)?.into_any()]; + for arg in args { + let arg = arg.into_pyobject(py)?; + all_args.push(arg); + } + let all_args = PyTuple::new(py, all_args)?; + + let kwargs = log_data + .map(|log_data| log_data.into_pyobject(py)) + .transpose()?; + + self.0.call_method(py, "info", all_args, kwargs.as_ref())?; + + Ok(()) + } + + pub fn warning<'py, ExcInfo: IntoPyObject<'py>>( + &self, + py: Python<'py>, + msg: &str, + args: Vec, + log_data: Option>, + ) -> PyResult<()> { + let mut all_args = vec![msg.into_pyobject(py)?.into_any()]; + for arg in args { + let arg = arg.into_pyobject(py)?; + all_args.push(arg); + } + let all_args = PyTuple::new(py, all_args)?; + + let kwargs = log_data + .map(|log_data| log_data.into_pyobject(py)) + .transpose()?; + + self.0 + .call_method(py, "warning", all_args, kwargs.as_ref())?; + + Ok(()) + } + + pub fn error<'py, ExcInfo: IntoPyObject<'py>>( + &self, + py: Python<'py>, + msg: &str, + args: Vec, + log_data: Option>, + ) -> PyResult<()> { + let mut all_args = vec![msg.into_pyobject(py)?.into_any()]; + for arg in args { + let arg = arg.into_pyobject(py)?; + all_args.push(arg); + } + let all_args = PyTuple::new(py, all_args)?; + + let kwargs = log_data + .map(|log_data| log_data.into_pyobject(py)) + .transpose()?; + + self.0.call_method(py, "error", all_args, kwargs.as_ref())?; + + Ok(()) + } + + pub fn critical<'py, ExcInfo: IntoPyObject<'py>>( + &self, + py: Python<'py>, + msg: &str, + args: Vec, + log_data: Option>, + ) -> PyResult<()> { + let mut all_args = vec![msg.into_pyobject(py)?.into_any()]; + for arg in args { + let arg = arg.into_pyobject(py)?; + all_args.push(arg); + } + let all_args = PyTuple::new(py, all_args)?; + + let kwargs = log_data + .map(|log_data| log_data.into_pyobject(py)) + .transpose()?; + + self.0 + .call_method(py, "critical", all_args, kwargs.as_ref())?; + + Ok(()) + } +} diff --git a/src/home_assistant/mod.rs b/src/home_assistant/mod.rs index 4a1e3cf..4282014 100644 --- a/src/home_assistant/mod.rs +++ b/src/home_assistant/mod.rs @@ -2,6 +2,7 @@ pub mod domain; pub mod entity_id; pub mod event; pub mod home_assistant; +pub mod logger; pub mod object_id; pub mod state; pub mod state_machine;