bear-utils 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. bear_utils/__init__.py +51 -0
  2. bear_utils/__main__.py +14 -0
  3. bear_utils/_internal/__init__.py +0 -0
  4. bear_utils/_internal/_version.py +1 -0
  5. bear_utils/_internal/cli.py +119 -0
  6. bear_utils/_internal/debug.py +174 -0
  7. bear_utils/ai/__init__.py +30 -0
  8. bear_utils/ai/ai_helpers/__init__.py +136 -0
  9. bear_utils/ai/ai_helpers/_common.py +19 -0
  10. bear_utils/ai/ai_helpers/_config.py +24 -0
  11. bear_utils/ai/ai_helpers/_parsers.py +194 -0
  12. bear_utils/ai/ai_helpers/_types.py +15 -0
  13. bear_utils/cache/__init__.py +131 -0
  14. bear_utils/cli/__init__.py +22 -0
  15. bear_utils/cli/_args.py +12 -0
  16. bear_utils/cli/_get_version.py +207 -0
  17. bear_utils/cli/commands.py +105 -0
  18. bear_utils/cli/prompt_helpers.py +186 -0
  19. bear_utils/cli/shell/__init__.py +1 -0
  20. bear_utils/cli/shell/_base_command.py +81 -0
  21. bear_utils/cli/shell/_base_shell.py +430 -0
  22. bear_utils/cli/shell/_common.py +19 -0
  23. bear_utils/cli/typer_bridge.py +90 -0
  24. bear_utils/config/__init__.py +13 -0
  25. bear_utils/config/config_manager.py +229 -0
  26. bear_utils/config/dir_manager.py +69 -0
  27. bear_utils/config/settings_manager.py +179 -0
  28. bear_utils/constants/__init__.py +90 -0
  29. bear_utils/constants/_exceptions.py +8 -0
  30. bear_utils/constants/_exit_code.py +60 -0
  31. bear_utils/constants/_http_status_code.py +37 -0
  32. bear_utils/constants/_lazy_typing.py +15 -0
  33. bear_utils/constants/_meta.py +196 -0
  34. bear_utils/constants/date_related.py +25 -0
  35. bear_utils/constants/time_related.py +24 -0
  36. bear_utils/database/__init__.py +8 -0
  37. bear_utils/database/_db_manager.py +98 -0
  38. bear_utils/events/__init__.py +18 -0
  39. bear_utils/events/events_class.py +52 -0
  40. bear_utils/events/events_module.py +74 -0
  41. bear_utils/extras/__init__.py +28 -0
  42. bear_utils/extras/_async_helpers.py +67 -0
  43. bear_utils/extras/_tools.py +185 -0
  44. bear_utils/extras/_zapper.py +399 -0
  45. bear_utils/extras/platform_utils.py +57 -0
  46. bear_utils/extras/responses/__init__.py +5 -0
  47. bear_utils/extras/responses/function_response.py +451 -0
  48. bear_utils/extras/wrappers/__init__.py +1 -0
  49. bear_utils/extras/wrappers/add_methods.py +100 -0
  50. bear_utils/extras/wrappers/string_io.py +46 -0
  51. bear_utils/files/__init__.py +6 -0
  52. bear_utils/files/file_handlers/__init__.py +5 -0
  53. bear_utils/files/file_handlers/_base_file_handler.py +107 -0
  54. bear_utils/files/file_handlers/file_handler_factory.py +280 -0
  55. bear_utils/files/file_handlers/json_file_handler.py +71 -0
  56. bear_utils/files/file_handlers/log_file_handler.py +40 -0
  57. bear_utils/files/file_handlers/toml_file_handler.py +76 -0
  58. bear_utils/files/file_handlers/txt_file_handler.py +76 -0
  59. bear_utils/files/file_handlers/yaml_file_handler.py +64 -0
  60. bear_utils/files/ignore_parser.py +293 -0
  61. bear_utils/graphics/__init__.py +6 -0
  62. bear_utils/graphics/bear_gradient.py +145 -0
  63. bear_utils/graphics/font/__init__.py +13 -0
  64. bear_utils/graphics/font/_raw_block_letters.py +463 -0
  65. bear_utils/graphics/font/_theme.py +31 -0
  66. bear_utils/graphics/font/_utils.py +220 -0
  67. bear_utils/graphics/font/block_font.py +192 -0
  68. bear_utils/graphics/font/glitch_font.py +63 -0
  69. bear_utils/graphics/image_helpers.py +45 -0
  70. bear_utils/gui/__init__.py +8 -0
  71. bear_utils/gui/gui_tools/__init__.py +10 -0
  72. bear_utils/gui/gui_tools/_settings.py +36 -0
  73. bear_utils/gui/gui_tools/_types.py +12 -0
  74. bear_utils/gui/gui_tools/qt_app.py +150 -0
  75. bear_utils/gui/gui_tools/qt_color_picker.py +130 -0
  76. bear_utils/gui/gui_tools/qt_file_handler.py +130 -0
  77. bear_utils/gui/gui_tools/qt_input_dialog.py +303 -0
  78. bear_utils/logger_manager/__init__.py +109 -0
  79. bear_utils/logger_manager/_common.py +63 -0
  80. bear_utils/logger_manager/_console_junk.py +135 -0
  81. bear_utils/logger_manager/_log_level.py +50 -0
  82. bear_utils/logger_manager/_styles.py +95 -0
  83. bear_utils/logger_manager/logger_protocol.py +42 -0
  84. bear_utils/logger_manager/loggers/__init__.py +1 -0
  85. bear_utils/logger_manager/loggers/_console.py +223 -0
  86. bear_utils/logger_manager/loggers/_level_sin.py +61 -0
  87. bear_utils/logger_manager/loggers/_logger.py +19 -0
  88. bear_utils/logger_manager/loggers/base_logger.py +244 -0
  89. bear_utils/logger_manager/loggers/base_logger.pyi +51 -0
  90. bear_utils/logger_manager/loggers/basic_logger/__init__.py +5 -0
  91. bear_utils/logger_manager/loggers/basic_logger/logger.py +80 -0
  92. bear_utils/logger_manager/loggers/basic_logger/logger.pyi +19 -0
  93. bear_utils/logger_manager/loggers/buffer_logger.py +57 -0
  94. bear_utils/logger_manager/loggers/console_logger.py +278 -0
  95. bear_utils/logger_manager/loggers/console_logger.pyi +50 -0
  96. bear_utils/logger_manager/loggers/fastapi_logger.py +333 -0
  97. bear_utils/logger_manager/loggers/file_logger.py +151 -0
  98. bear_utils/logger_manager/loggers/simple_logger.py +98 -0
  99. bear_utils/logger_manager/loggers/sub_logger.py +105 -0
  100. bear_utils/logger_manager/loggers/sub_logger.pyi +23 -0
  101. bear_utils/monitoring/__init__.py +13 -0
  102. bear_utils/monitoring/_common.py +28 -0
  103. bear_utils/monitoring/host_monitor.py +346 -0
  104. bear_utils/time/__init__.py +59 -0
  105. bear_utils-0.0.1.dist-info/METADATA +305 -0
  106. bear_utils-0.0.1.dist-info/RECORD +107 -0
  107. bear_utils-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,278 @@
1
+ """ConsoleLogger: A comprehensive console logger that combines Python's logging framework with Rich console styling."""
2
+
3
+ # region Imports
4
+ from contextlib import suppress
5
+ from functools import cached_property
6
+ from logging import DEBUG, Formatter, Handler, Logger
7
+ from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
8
+ from queue import Queue
9
+ from typing import TYPE_CHECKING, override
10
+
11
+ from prompt_toolkit import PromptSession
12
+ from rich.text import Text
13
+ from rich.theme import Theme
14
+
15
+ from bear_utils.constants.date_related import DATE_TIME_FORMAT
16
+ from bear_utils.logger_manager._common import FIVE_MEGABYTES, VERBOSE_CONSOLE_FORMAT, VERBOSE_FORMAT, ExecValues
17
+ from bear_utils.logger_manager._console_junk import ConsoleBuffering, ConsoleFormatter, ConsoleHandler
18
+
19
+ from .base_logger import BaseLogger
20
+
21
+ if TYPE_CHECKING:
22
+ from rich.traceback import Traceback
23
+
24
+ # endregion Imports
25
+
26
+
27
+ class ConsoleLogger(Logger, BaseLogger):
28
+ """A comprehensive console logger that combines Python's logging framework with Rich console styling.
29
+
30
+ This logger provides styled console output with configurable file logging, queue handling,
31
+ buffering, and interactive input capabilities. It dynamically creates logging methods
32
+ (info, error, debug, etc.) that forward to Rich's styled console printing.
33
+
34
+ Features:
35
+ - Rich styled console output with themes
36
+ - Optional file logging with rotation
37
+ - Queue-based async logging
38
+ - Message buffering capabilities
39
+ - Interactive prompt integration
40
+ - Exception tracebacks with local variables
41
+
42
+ Example:
43
+ logger = ConsoleLogger.get_instance(init=True, verbose=True, name="MyLogger", level=DEBUG)
44
+ logger.info("This is styled info")
45
+ logger.error("This is styled error")
46
+ logger.success("This is styled success")
47
+ """
48
+
49
+ # region Setup
50
+ def __init__(
51
+ self,
52
+ theme: Theme | None = None,
53
+ name: str = "ConsoleLogger",
54
+ level: int = DEBUG,
55
+ disabled: bool = True,
56
+ console: bool = True,
57
+ file: bool = False,
58
+ queue_handler: bool = False,
59
+ buffering: bool = False,
60
+ *_,
61
+ **kwargs,
62
+ ) -> None:
63
+ """Initialize the ConsoleLogger with optional file, console, and buffering settings."""
64
+ Logger.__init__(self, name=name, level=level)
65
+ BaseLogger.__init__(
66
+ self,
67
+ output_handler=self._console_output,
68
+ theme=theme,
69
+ style_disabled=kwargs.get("style_disabled", False),
70
+ logger_mode=kwargs.get("logger_mode", True),
71
+ level=level,
72
+ )
73
+ self.name = name
74
+ self.level = level
75
+ self.setLevel(level)
76
+ self.session = None
77
+ self.disabled = disabled
78
+ self._handlers: list[Handler] = []
79
+ self.logger_mode: bool = kwargs.pop("logger_mode", True)
80
+ if self.logger_mode:
81
+ self.disabled = False
82
+ self._handle_enable_booleans(
83
+ file=file,
84
+ console=console,
85
+ buffering=buffering,
86
+ queue_handler=queue_handler,
87
+ **kwargs,
88
+ )
89
+
90
+ def _handle_enable_booleans(
91
+ self,
92
+ file: bool,
93
+ console: bool,
94
+ buffering: bool,
95
+ queue_handler: bool,
96
+ **kwargs,
97
+ ) -> None:
98
+ """Configure logging handlers based on initialization parameters."""
99
+ if console or buffering:
100
+ self.console_handler: ConsoleHandler = ConsoleHandler(self.print, self.output_buffer)
101
+ self.console_handler.setFormatter(ConsoleFormatter(fmt=VERBOSE_CONSOLE_FORMAT, datefmt=DATE_TIME_FORMAT))
102
+ self.console_handler.setLevel(self.level)
103
+ if console:
104
+ self._handlers.append(self.console_handler)
105
+ if buffering:
106
+ self.buffer_handler: ConsoleBuffering = ConsoleBuffering(console_handler=self.console_handler)
107
+ self.addHandler(self.buffer_handler)
108
+ if file:
109
+ self.file_handler: RotatingFileHandler = RotatingFileHandler(
110
+ filename=kwargs.get("file_path", "console.log"),
111
+ maxBytes=kwargs.get("max_bytes", FIVE_MEGABYTES),
112
+ backupCount=kwargs.get("backup_count", 5),
113
+ )
114
+ self.file_handler.setFormatter(Formatter(fmt=VERBOSE_FORMAT, datefmt=DATE_TIME_FORMAT))
115
+ self.file_handler.setLevel(self.level)
116
+ self._handlers.append(self.file_handler)
117
+ if queue_handler:
118
+ self.queue = Queue()
119
+ self.queue_handler = QueueHandler(self.queue)
120
+ self.addHandler(self.queue_handler)
121
+ self.listener = QueueListener(self.queue, *self._handlers)
122
+ self.listener.start()
123
+ else:
124
+ for handler in self._handlers:
125
+ self.addHandler(handler)
126
+
127
+ def stop_queue_listener(self) -> None:
128
+ """Stop the queue listener if it exists and clean up resources."""
129
+ if hasattr(self, "listener"):
130
+ self.verbose("ConsoleLogger: QueueListener stopped and cleaned up.")
131
+ self.listener.stop()
132
+ del self.listener
133
+ del self.queue
134
+ del self.queue_handler
135
+
136
+ def trigger_buffer_flush(self) -> Text:
137
+ """Flush buffered messages to console output."""
138
+ if hasattr(self, "buffer_handler"):
139
+ return self.buffer_handler.flush_to_output()
140
+ return Text("No buffering handler available.", style="bold red")
141
+
142
+ def set_base_level(self, level: int) -> None:
143
+ """Set the base logging level for the console logger."""
144
+ super().set_base_level(level)
145
+ self.setLevel(level)
146
+ if hasattr(self, "console_handler"):
147
+ self.console_handler.setLevel(level)
148
+ if hasattr(self, "buffer_handler"):
149
+ self.buffer_handler.setLevel(level)
150
+ if hasattr(self, "queue_handler"):
151
+ self.queue_handler.setLevel(level)
152
+
153
+ def _console_output(self, msg: object, extra: dict, *args, **kwargs) -> None:
154
+ """Console-specific output handler that integrates with logging module."""
155
+ if not self.logger_mode:
156
+ self.print(msg, *args, **kwargs)
157
+ else:
158
+ kwargs.pop("style", None)
159
+ self.log(
160
+ extra.get("log_level", DEBUG),
161
+ msg,
162
+ *args,
163
+ extra=extra,
164
+ **kwargs,
165
+ )
166
+
167
+ # endregion Setup
168
+
169
+ # region Utility Methods
170
+
171
+ async def input(self, msg: str, style: str = "info", **kwargs) -> str:
172
+ """Display a styled prompt and return user input asynchronously."""
173
+ if not self.session:
174
+ self.session = PromptSession(**kwargs)
175
+ self.print(msg, style=style)
176
+ return await self.session.prompt_async()
177
+
178
+ def output_buffer(
179
+ self,
180
+ msg: object,
181
+ end: str = "\n",
182
+ exc_info: str | None = None,
183
+ exec_values: ExecValues | None = None,
184
+ *_,
185
+ **kwargs,
186
+ ) -> str:
187
+ """Capture console output to a string buffer without printing to terminal."""
188
+ if exc_info and exec_values:
189
+ exception: Traceback = self._get_exception(manual=True, exec_values=exec_values)
190
+ self.console.print(exception, end=end)
191
+ self.console.print(msg, end="", style=kwargs.get("style", "info"))
192
+ output = self.console_buffer.getvalue()
193
+ self._reset_buffer()
194
+ return output
195
+
196
+ # endregion Utility Methods
197
+
198
+ # region Enhanced Print Methods
199
+
200
+ def print(
201
+ self,
202
+ msg: object,
203
+ end: str = "\n",
204
+ exc_info: str | None = None,
205
+ extra: dict | None = None,
206
+ *args,
207
+ **kwargs,
208
+ ) -> None | str:
209
+ """Print styled messages with enhanced exception handling and JSON support.
210
+
211
+ Extends the base print method with proper exception tracebacks and
212
+ integrated JSON printing for structured data output.
213
+ """
214
+ if exc_info is not None:
215
+ with suppress(ValueError):
216
+ self._print(self._get_exception(), end=end, width=100, show_locals=True, **kwargs)
217
+
218
+ self._print(msg, end, *args, **kwargs)
219
+
220
+ if extra:
221
+ self._print(msg=extra, end=end, json=True, indent=4)
222
+
223
+ @cached_property
224
+ def stack_level(self) -> int:
225
+ """Cached property to retrieve the current stack level."""
226
+ return self.stack_tracker.record_end()
227
+
228
+ @override
229
+ def _log( # type: ignore[override]
230
+ self,
231
+ level: int,
232
+ msg: object,
233
+ args: tuple,
234
+ exc_info: str | None = None,
235
+ extra: dict | None = None,
236
+ stack_info: bool = False,
237
+ stacklevel: int | None = None,
238
+ ) -> None:
239
+ """Custom logging implementation with enhanced exception handling.
240
+
241
+ Overrides the standard logging._log method to provide better exception
242
+ value extraction for Rich traceback integration while respecting log levels.
243
+ """
244
+ stacklevel = stacklevel or self.stack_level
245
+ try:
246
+ fn, lno, func, sinfo = self.findCaller(stack_info, stacklevel)
247
+ except ValueError:
248
+ fn, lno, func, sinfo = "(unknown file)", 0, "(unknown function)", None
249
+ final_extra = extra or {}
250
+ if exc_info is not None:
251
+ exec_values = self._extract_exception_values(exc_info)
252
+ if exec_values:
253
+ final_extra = {**final_extra, "exec_values": exec_values}
254
+
255
+ record = self.makeRecord(
256
+ name=self.name,
257
+ level=level,
258
+ fn=fn,
259
+ lno=lno,
260
+ msg=msg,
261
+ args=args,
262
+ exc_info=None,
263
+ func=func,
264
+ extra=final_extra,
265
+ sinfo=sinfo,
266
+ )
267
+
268
+ self.handle(record)
269
+
270
+ def exit(self) -> None:
271
+ """Clean up resources including queue listeners and console buffers."""
272
+ if hasattr(self, "queue_handler"):
273
+ self.queue_handler.flush()
274
+ self.stop_queue_listener()
275
+
276
+ self.console_buffer.close()
277
+
278
+ # endregion Enhanced Print Methods
@@ -0,0 +1,50 @@
1
+ from logging import Logger
2
+ from logging.handlers import QueueHandler, RotatingFileHandler
3
+ from typing import Any
4
+
5
+ from rich.text import Text
6
+ from rich.theme import Theme
7
+
8
+ from bear_utils.logger_manager._console_junk import ConsoleBuffering, ConsoleHandler
9
+
10
+ from .base_logger import BaseLogger
11
+ from .sub_logger import SubConsoleLogger
12
+
13
+ class ConsoleLogger(Logger, BaseLogger):
14
+ name: str
15
+ level: int
16
+ file: bool
17
+ queue_handler: QueueHandler
18
+ buffer_handler: ConsoleBuffering
19
+ console_handler: ConsoleHandler
20
+ file_handler: RotatingFileHandler
21
+ sub_logger: dict[str, SubConsoleLogger[ConsoleLogger]]
22
+
23
+ def __init__(
24
+ self,
25
+ theme: Theme | None = None,
26
+ name: str = "ConsoleLogger",
27
+ level: int = ...,
28
+ disabled: bool = True,
29
+ queue_handler: bool = False,
30
+ buffering: bool = False,
31
+ file: bool = False,
32
+ console: bool = True,
33
+ style_disabled: bool = False,
34
+ logger_mode: bool = True,
35
+ *args,
36
+ **kwargs,
37
+ ) -> None: ...
38
+ # fmt: off
39
+ def debug(self, msg: object, *args, **kwargs: Any) -> None: ...
40
+ def info(self, msg: object, *args, **kwargs: Any) -> None: ...
41
+ def warning(self, msg: object, *args, **kwargs: Any) -> None: ...
42
+ def error(self, msg: object, *args, **kwargs: Any) -> None: ...
43
+ def success(self, msg: object, *args, **kwargs: Any) -> None: ...
44
+ def failure(self, msg: object, *args, **kwargs: Any) -> None: ...
45
+ def verbose(self, msg: object, *args, **kwargs: Any) -> None: ...
46
+ def set_base_level(self, level: int) -> None: ...
47
+ def input(self, msg: str, style: str = "info") -> str: ...
48
+ def trigger_buffer_flush(self) -> str | Text: ...
49
+ def print(self, msg: object, end: str="\n", exc_info:str|None=None, extra: dict | None = None, *args, **kwargs) -> None | str: ...
50
+ def exit(self) -> None: ...
@@ -0,0 +1,333 @@
1
+ """FastAPI-based local logging server and HTTP logger."""
2
+
3
+ import asyncio
4
+ from collections import deque
5
+ from pathlib import Path
6
+ import threading
7
+ from typing import TYPE_CHECKING, Any, Self, TextIO
8
+
9
+ from fastapi import FastAPI
10
+ from fastapi.responses import JSONResponse
11
+ from httpx import AsyncClient
12
+ from pydantic import BaseModel, Field, field_serializer
13
+ from singleton_base import SingletonBase
14
+ import uvicorn
15
+
16
+ from bear_utils.constants import DEVNULL, ExitCode, HTTPStatusCode
17
+ from bear_utils.logger_manager._log_level import LogLevel
18
+ from bear_utils.time import EpochTimestamp
19
+
20
+ if TYPE_CHECKING:
21
+ from httpx import Response
22
+
23
+
24
+ VERBOSE: LogLevel = LogLevel.from_name("VERBOSE")
25
+ DEBUG: LogLevel = LogLevel.from_name("DEBUG")
26
+ INFO: LogLevel = LogLevel.from_name("INFO")
27
+ WARNING: LogLevel = LogLevel.from_name("WARNING")
28
+ ERROR: LogLevel = LogLevel.from_name("ERROR")
29
+ FAILURE: LogLevel = LogLevel.from_name("FAILURE")
30
+ SUCCESS: LogLevel = LogLevel.from_name("SUCCESS")
31
+
32
+
33
+ class LogRequest(BaseModel):
34
+ """Request model for logging messages."""
35
+
36
+ level: LogLevel | int | str = Field(default=DEBUG, description="Log level of the message")
37
+ message: str = Field(default="", description="Log message content")
38
+ args: tuple = Field(default=(), description="Arguments for the log message")
39
+ kwargs: dict[str, str] = Field(default_factory=dict, description="Keyword arguments for the log message")
40
+
41
+ model_config = {
42
+ "arbitrary_types_allowed": True,
43
+ }
44
+
45
+ @field_serializer("level")
46
+ def serialize_level(self, value: LogLevel | int | str) -> int:
47
+ """Serialize the log level to an integer."""
48
+ return LogLevel.get(value, default=DEBUG).value
49
+
50
+
51
+ class LoggingServer[T: TextIO](SingletonBase):
52
+ """A local server that writes logs to a file."""
53
+
54
+ def __init__(
55
+ self,
56
+ host: str = "localhost",
57
+ port: int = 8080,
58
+ log_file: str | Path = "server.log",
59
+ level: LogLevel | int | str = DEBUG,
60
+ file: T = DEVNULL, # Default to DEVNULL to discard console output
61
+ maxlen: int = 100,
62
+ ) -> None:
63
+ """Initialize the logging server."""
64
+ self.host: str = host
65
+ self.port: int = port
66
+ self.log_file: Path = Path(log_file)
67
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
68
+ self.level: LogLevel = LogLevel.get(level, default=DEBUG)
69
+ self.app = FastAPI()
70
+ self.server_thread = None
71
+ self._running = False
72
+ self.logs: deque[LogRequest] = deque(maxlen=maxlen)
73
+ self.file: T = file
74
+ self._setup_routes()
75
+
76
+ @property
77
+ def running(self) -> bool:
78
+ """Check if the server is running."""
79
+ return self._running or (self.server_thread is not None and self.server_thread.is_alive())
80
+
81
+ def __len__(self) -> int:
82
+ """Get the number of logged messages."""
83
+ return len(self.logs)
84
+
85
+ def get_logs(self) -> list[LogRequest]:
86
+ """Get the list of logged messages."""
87
+ return list(self.logs)
88
+
89
+ def print(self, msg: object, end: str = "\n") -> None:
90
+ """Print the message to the specified file with an optional end character."""
91
+ print(msg, end=end, file=self.file)
92
+
93
+ def response(
94
+ self,
95
+ status: str,
96
+ message: str = "",
97
+ status_code: HTTPStatusCode = HTTPStatusCode.SERVER_OK,
98
+ ) -> JSONResponse:
99
+ """Create a JSON response with the given content and status code."""
100
+ return JSONResponse(content={"status": status, "message": message}, status_code=status_code.value)
101
+
102
+ def _setup_routes(self) -> None:
103
+ """Set up the FastAPI routes for logging and health check."""
104
+
105
+ @self.app.post("/log")
106
+ async def log_message(request: LogRequest | Any) -> JSONResponse:
107
+ """Endpoint to log a message."""
108
+ request = LogRequest(
109
+ level=request["level"] if isinstance(request, dict) else request.level,
110
+ message=request["message"] if isinstance(request, dict) else request.message,
111
+ args=request["args"] if isinstance(request, dict) else request.args,
112
+ kwargs=request["kwargs"] if isinstance(request, dict) else request.kwargs,
113
+ )
114
+ level: LogLevel = LogLevel.get(request.level, default=DEBUG)
115
+ if level.value < self.level.value:
116
+ return self.response(status="ignored", message="Log level is lower than server's minimum level")
117
+ message = request.message
118
+ args = request.args
119
+ kwargs: dict[str, str] | Any = request.kwargs
120
+ success: ExitCode = self.write_log(level, message, *args, **kwargs)
121
+ self.logs.append(request)
122
+ if success != ExitCode.SUCCESS:
123
+ return self.response(
124
+ status="error", message="Failed to write log", status_code=HTTPStatusCode.SERVER_ERROR
125
+ )
126
+ return self.response(status="success", status_code=HTTPStatusCode.SERVER_OK)
127
+
128
+ @self.app.get("/health")
129
+ async def health_check() -> JSONResponse:
130
+ return JSONResponse(
131
+ content={"status": "healthy"},
132
+ status_code=HTTPStatusCode.SERVER_OK,
133
+ )
134
+
135
+ def write_log(
136
+ self,
137
+ level: LogLevel,
138
+ message: str,
139
+ end: str = "\n",
140
+ *args,
141
+ **kwargs,
142
+ ) -> ExitCode:
143
+ """Write a log entry to the file - same logic as original logger."""
144
+ timestamp: str = EpochTimestamp.now().to_string()
145
+ log_entry: str = f"[{timestamp}] {level}: {message}"
146
+ buffer = []
147
+ try:
148
+ buffer.append(log_entry)
149
+ if args:
150
+ buffer.append(f"{end}".join(str(arg) for arg in args))
151
+ if kwargs:
152
+ for key, value in kwargs.items():
153
+ buffer.append(f"{key}={value}{end}")
154
+ if kwargs.pop("console", False):
155
+ self.print(f"{end}".join(buffer))
156
+ with open(self.log_file, "a", encoding="utf-8") as f:
157
+ for line in buffer:
158
+ f.write(f"{line}{end}")
159
+ return ExitCode.SUCCESS
160
+ except Exception:
161
+ self.print(f"[{timestamp}] {level}: {message}")
162
+ return ExitCode.FAILURE
163
+
164
+ async def start(self) -> None:
165
+ """Start the logging server in a separate thread."""
166
+ if self._running:
167
+ return
168
+
169
+ def _run_server() -> None:
170
+ """Run the FastAPI server in a new event loop."""
171
+ uvicorn.run(self.app, host=self.host, port=self.port, log_level="error")
172
+
173
+ self.server_thread = threading.Thread(target=_run_server)
174
+ self.server_thread.daemon = True
175
+ self.server_thread.start()
176
+ self._running = True
177
+ self.write_log(DEBUG, f"Logging server started on {self.host}:{self.port}")
178
+
179
+ async def stop(self) -> None:
180
+ """Stop the logging server."""
181
+ if self._running:
182
+ self._running = False
183
+ if self.server_thread is not None:
184
+ self.server_thread.join(timeout=1)
185
+ self.server_thread = None
186
+ self.write_log(DEBUG, "Logging server stopped")
187
+
188
+ async def __aenter__(self) -> Self:
189
+ """Start the logging server."""
190
+ if not self.running:
191
+ await self.start()
192
+ else:
193
+ self.write_log(DEBUG, "Logging server is already running")
194
+ return self
195
+
196
+ async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
197
+ """Stop the logging server."""
198
+ await self.stop()
199
+
200
+
201
+ class LoggingClient[T: TextIO]:
202
+ """Logger that calls HTTP endpoints but behaves like SimpleLogger."""
203
+
204
+ def __init__(
205
+ self,
206
+ server_url: str | None = None,
207
+ host: str = "http://localhost",
208
+ port: int = 8080,
209
+ level: LogLevel | int | str = DEBUG,
210
+ file: T = DEVNULL, # Default to DEVNULL to discard console output
211
+ ) -> None:
212
+ """Initialize the ServerLogger."""
213
+ self.host: str = host
214
+ self.port: int = port
215
+ self.server_url: str = server_url or f"{self.host}:{self.port}"
216
+ self.level: LogLevel = LogLevel.get(level, default=DEBUG)
217
+ self.client: AsyncClient = AsyncClient(timeout=5.0)
218
+ self.file: T = file
219
+
220
+ async def post(self, url: str, json: dict) -> "Response":
221
+ """Send a POST request to the server."""
222
+ return await self.client.post(url=url, json=json)
223
+
224
+ async def _log(self, request: LogRequest) -> None:
225
+ """Same interface as SimpleLogger._log but calls HTTP endpoint."""
226
+ try:
227
+ response: Response = await self.post(url=f"{self.server_url}/log", json=request.model_dump())
228
+ if response.status_code != HTTPStatusCode.SERVER_OK:
229
+ await self._fallback_log(str(request.level), request.message, *request.args, **request.kwargs)
230
+ except Exception:
231
+ await self._fallback_log(str(request.level), request.message, *request.args, **request.kwargs)
232
+
233
+ async def log(self, level: LogLevel, msg: object, *args: Any, **kwargs: Any) -> None:
234
+ """Log a message at the specified level."""
235
+ if level.value >= self.level.value:
236
+ request = LogRequest(level=level.value, message=str(msg), args=args, kwargs=kwargs)
237
+ await self._log(request)
238
+
239
+ async def _fallback_log(self, lvl: str, msg: object, *args: Any, **kwargs: Any) -> None:
240
+ """Fallback - same as original SimpleLogger._log."""
241
+ timestamp: str = EpochTimestamp.now().to_string()
242
+ print(f"Fallback Logging: [{timestamp}] {lvl}: {msg}", file=self.file)
243
+ if args:
244
+ print(" ".join(str(arg) for arg in args), file=self.file)
245
+ if kwargs:
246
+ for key, value in kwargs.items():
247
+ print(f"{key}={value}", file=self.file)
248
+
249
+ async def verbose(self, msg: object, *args, **kwargs) -> None:
250
+ """Log a verbose message."""
251
+ await self.log(VERBOSE, msg, *args, **kwargs)
252
+
253
+ async def debug(self, msg: object, *args, **kwargs) -> None:
254
+ """Log a debug message."""
255
+ await self.log(DEBUG, msg, *args, **kwargs)
256
+
257
+ async def info(self, msg: object, *args, **kwargs) -> None:
258
+ """Log an info message."""
259
+ await self.log(INFO, msg, *args, **kwargs)
260
+
261
+ async def warning(self, msg: object, *args, **kwargs) -> None:
262
+ """Log a warning message."""
263
+ await self.log(WARNING, msg, *args, **kwargs)
264
+
265
+ async def error(self, msg: object, *args, **kwargs) -> None:
266
+ """Log an error message."""
267
+ await self.log(ERROR, msg, *args, **kwargs)
268
+
269
+ async def failure(self, msg: object, *args, **kwargs) -> None:
270
+ """Log a failure message."""
271
+ await self.log(FAILURE, msg, *args, **kwargs)
272
+
273
+ async def success(self, msg: object, *args, **kwargs) -> None:
274
+ """Log a success message."""
275
+ await self.log(SUCCESS, msg, *args, **kwargs)
276
+
277
+ async def close(self) -> None:
278
+ """Close the HTTP client."""
279
+ await self.client.aclose()
280
+
281
+ async def __aenter__(self) -> Self:
282
+ """Enter the asynchronous context manager."""
283
+ return self
284
+
285
+ async def __aexit__(self, exc_type: object, exc_value: object, traceback: object) -> None:
286
+ """Exit the asynchronous context manager."""
287
+ await self.close()
288
+
289
+
290
+ async def run_server(
291
+ host: str = "localhost",
292
+ port: int = 8080,
293
+ log_file: str = "server.log",
294
+ level: LogLevel | int | str = DEBUG,
295
+ file: TextIO = DEVNULL,
296
+ ) -> ExitCode | int:
297
+ """Run the local logging server."""
298
+ server: LoggingServer[TextIO] = LoggingServer(
299
+ host=host,
300
+ port=port,
301
+ log_file=log_file,
302
+ level=level,
303
+ file=file,
304
+ )
305
+ try:
306
+ while True:
307
+ await server.start()
308
+ except KeyboardInterrupt:
309
+ print("Stopping server...", file=server.file)
310
+ except Exception as e:
311
+ print(f"An error occurred: {e}", file=server.file)
312
+ return ExitCode.FAILURE
313
+ finally:
314
+ await server.stop()
315
+ return ExitCode.SUCCESS
316
+
317
+
318
+ def sync_server(
319
+ host: str = "localhost",
320
+ port: int = 8080,
321
+ log_file: str = "server.log",
322
+ level: LogLevel | int | str = DEBUG,
323
+ file: TextIO = DEVNULL,
324
+ ) -> ExitCode | int:
325
+ """Run the local logging server synchronously."""
326
+ loop = asyncio.get_event_loop()
327
+ asyncio.set_event_loop(loop)
328
+ return loop.run_until_complete(run_server(host, port, log_file, level, file))
329
+
330
+
331
+ # if __name__ == "__main__":
332
+ # exit_code = sync_server()
333
+ # sys.exit(exit_code)