bear-utils 0.8.26__py3-none-any.whl → 0.9.0__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.
- bear_utils/__init__.py +10 -4
- bear_utils/cli/__init__.py +5 -0
- bear_utils/cli/_args.py +12 -0
- bear_utils/cli/shell/_base_shell.py +4 -17
- bear_utils/constants/__init__.py +52 -1
- bear_utils/constants/_exit_code.py +60 -0
- bear_utils/constants/_http_status_code.py +37 -0
- bear_utils/constants/_meta.py +107 -0
- bear_utils/extras/responses/__init__.py +2 -6
- bear_utils/extras/responses/function_response.py +0 -4
- bear_utils/graphics/font/__init__.py +11 -0
- bear_utils/graphics/font/_raw_block_letters.py +463 -0
- bear_utils/graphics/font/_theme.py +11 -0
- bear_utils/graphics/font/block_font.py +150 -0
- bear_utils/graphics/font/glitch_font.py +63 -0
- bear_utils/logger_manager/__init__.py +15 -3
- bear_utils/logger_manager/_log_level.py +12 -88
- bear_utils/logger_manager/logger_protocol.py +22 -25
- bear_utils/logger_manager/loggers/_console.py +204 -0
- bear_utils/logger_manager/loggers/_logger.py +19 -0
- bear_utils/logger_manager/loggers/base_logger.py +7 -7
- bear_utils/logger_manager/loggers/fastapi_logger.py +235 -110
- bear_utils/logger_manager/loggers/simple_logger.py +39 -24
- {bear_utils-0.8.26.dist-info → bear_utils-0.9.0.dist-info}/METADATA +3 -3
- {bear_utils-0.8.26.dist-info → bear_utils-0.9.0.dist-info}/RECORD +26 -16
- bear_utils/constants/server.py +0 -16
- {bear_utils-0.8.26.dist-info → bear_utils-0.9.0.dist-info}/WHEEL +0 -0
@@ -1,209 +1,334 @@
|
|
1
1
|
"""FastAPI-based local logging server and HTTP logger."""
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import
|
4
|
+
from collections import deque
|
5
|
+
from pathlib import Path
|
5
6
|
import threading
|
6
|
-
from typing import TYPE_CHECKING
|
7
|
+
from typing import TYPE_CHECKING, Any, Self, TextIO
|
7
8
|
|
8
9
|
from fastapi import FastAPI
|
10
|
+
from fastapi.responses import JSONResponse
|
9
11
|
from httpx import AsyncClient
|
10
|
-
from pydantic import BaseModel
|
12
|
+
from pydantic import BaseModel, Field, field_serializer
|
13
|
+
from singleton_base import SingletonBase
|
11
14
|
import uvicorn
|
12
15
|
|
13
|
-
from bear_utils.constants import
|
14
|
-
from bear_utils.logger_manager._log_level import LogLevel
|
16
|
+
from bear_utils.constants import DEVNULL, ExitCode, HTTPStatusCode
|
17
|
+
from bear_utils.logger_manager._log_level import LogLevel
|
15
18
|
from bear_utils.time import EpochTimestamp
|
16
19
|
|
17
20
|
if TYPE_CHECKING:
|
18
21
|
from httpx import Response
|
19
22
|
|
20
23
|
|
21
|
-
VERBOSE: LogLevel =
|
22
|
-
DEBUG: LogLevel =
|
23
|
-
INFO: LogLevel =
|
24
|
-
WARNING: LogLevel =
|
25
|
-
ERROR: LogLevel =
|
26
|
-
|
27
|
-
|
28
|
-
def get_level(level: str) -> int:
|
29
|
-
"""Get the numeric value for a given level string."""
|
30
|
-
return log_levels.get_int(level)
|
31
|
-
|
32
|
-
|
33
|
-
def get_name(level: int) -> str:
|
34
|
-
"""Get the name of a logging level by its integer value."""
|
35
|
-
return log_levels.get_name(level)
|
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")
|
36
31
|
|
37
32
|
|
38
33
|
class LogRequest(BaseModel):
|
39
34
|
"""Request model for logging messages."""
|
40
35
|
|
41
|
-
level: str
|
42
|
-
message: str
|
43
|
-
args:
|
44
|
-
kwargs: dict[str, str] =
|
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")
|
45
40
|
|
41
|
+
model_config = {
|
42
|
+
"arbitrary_types_allowed": True,
|
43
|
+
}
|
46
44
|
|
47
|
-
|
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).value
|
49
|
+
|
50
|
+
|
51
|
+
class LoggingServer[T: TextIO](SingletonBase):
|
48
52
|
"""A local server that writes logs to a file."""
|
49
53
|
|
50
54
|
def __init__(
|
51
55
|
self,
|
52
56
|
host: str = "localhost",
|
53
57
|
port: int = 8080,
|
54
|
-
log_file: str = "server.log",
|
55
|
-
|
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,
|
56
62
|
) -> None:
|
57
63
|
"""Initialize the logging server."""
|
58
64
|
self.host: str = host
|
59
65
|
self.port: int = port
|
60
|
-
self.log_file:
|
61
|
-
self.
|
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)
|
62
69
|
self.app = FastAPI()
|
63
70
|
self.server_thread = None
|
64
71
|
self._running = False
|
72
|
+
self.logs: deque[LogRequest] = deque(maxlen=maxlen)
|
73
|
+
self.file: T = file
|
65
74
|
self._setup_routes()
|
66
|
-
|
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)
|
67
101
|
|
68
102
|
def _setup_routes(self) -> None:
|
69
103
|
"""Set up the FastAPI routes for logging and health check."""
|
70
104
|
|
71
105
|
@self.app.post("/log")
|
72
|
-
async def log_message(request: LogRequest) ->
|
73
|
-
|
74
|
-
|
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)
|
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)
|
75
127
|
|
76
128
|
@self.app.get("/health")
|
77
|
-
async def health_check() ->
|
78
|
-
return
|
129
|
+
async def health_check() -> JSONResponse:
|
130
|
+
return JSONResponse(
|
131
|
+
content={"status": "healthy"},
|
132
|
+
status_code=HTTPStatusCode.SERVER_OK,
|
133
|
+
)
|
79
134
|
|
80
|
-
def write_log(
|
135
|
+
def write_log(
|
136
|
+
self,
|
137
|
+
level: LogLevel,
|
138
|
+
message: str,
|
139
|
+
end: str = "\n",
|
140
|
+
console: bool = True,
|
141
|
+
*args,
|
142
|
+
**kwargs,
|
143
|
+
) -> ExitCode:
|
81
144
|
"""Write a log entry to the file - same logic as original logger."""
|
82
145
|
timestamp: str = EpochTimestamp.now().to_string()
|
146
|
+
log_entry: str = f"[{timestamp}] {level}: {message}"
|
147
|
+
buffer = []
|
83
148
|
try:
|
84
|
-
|
85
|
-
if
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
f.
|
95
|
-
|
149
|
+
buffer.append(log_entry)
|
150
|
+
if args:
|
151
|
+
buffer.append(f"{end}".join(str(arg) for arg in args))
|
152
|
+
if kwargs:
|
153
|
+
for key, value in kwargs.items():
|
154
|
+
buffer.append(f"{key}={value}{end}")
|
155
|
+
if console:
|
156
|
+
self.print(f"{end}".join(buffer))
|
157
|
+
with open(self.log_file, "a", encoding="utf-8") as f:
|
158
|
+
for line in buffer:
|
159
|
+
f.write(f"{line}{end}")
|
160
|
+
return ExitCode.SUCCESS
|
96
161
|
except Exception:
|
97
|
-
print(f"[{timestamp}] {level}: {message}"
|
98
|
-
|
99
|
-
self.buffer.clear()
|
162
|
+
self.print(f"[{timestamp}] {level}: {message}")
|
163
|
+
return ExitCode.FAILURE
|
100
164
|
|
101
|
-
def start(self) -> None:
|
165
|
+
async def start(self) -> None:
|
102
166
|
"""Start the logging server in a separate thread."""
|
103
167
|
if self._running:
|
104
168
|
return
|
105
169
|
|
106
|
-
def
|
170
|
+
def _run_server() -> None:
|
107
171
|
"""Run the FastAPI server in a new event loop."""
|
108
|
-
|
109
|
-
uvicorn.run(self.app, host=self.host, port=self.port, log_level="warning")
|
172
|
+
uvicorn.run(self.app, host=self.host, port=self.port, log_level="error")
|
110
173
|
|
111
|
-
self.server_thread = threading.Thread(target=
|
174
|
+
self.server_thread = threading.Thread(target=_run_server)
|
112
175
|
self.server_thread.daemon = True
|
113
176
|
self.server_thread.start()
|
114
177
|
self._running = True
|
115
|
-
|
178
|
+
self.write_log(DEBUG, f"Logging server started on {self.host}:{self.port}")
|
116
179
|
|
117
|
-
def stop(self) -> None:
|
180
|
+
async def stop(self) -> None:
|
118
181
|
"""Stop the logging server."""
|
119
182
|
if self._running:
|
120
183
|
self._running = False
|
121
|
-
|
184
|
+
if self.server_thread is not None:
|
185
|
+
self.server_thread.join(timeout=1)
|
186
|
+
self.server_thread = None
|
187
|
+
self.write_log(DEBUG, "Logging server stopped")
|
188
|
+
|
189
|
+
async def __aenter__(self) -> Self:
|
190
|
+
"""Start the logging server."""
|
191
|
+
if not self.running:
|
192
|
+
await self.start()
|
193
|
+
else:
|
194
|
+
self.write_log(DEBUG, "Logging server is already running")
|
195
|
+
return self
|
196
|
+
|
197
|
+
async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
198
|
+
"""Stop the logging server."""
|
199
|
+
await self.stop()
|
122
200
|
|
123
201
|
|
124
|
-
class
|
202
|
+
class LoggingClient[T: TextIO]:
|
125
203
|
"""Logger that calls HTTP endpoints but behaves like SimpleLogger."""
|
126
204
|
|
127
|
-
def __init__(
|
205
|
+
def __init__(
|
206
|
+
self,
|
207
|
+
server_url: str | None = None,
|
208
|
+
host: str = "http://localhost",
|
209
|
+
port: int = 8080,
|
210
|
+
level: LogLevel | int | str = INFO,
|
211
|
+
file: T = DEVNULL, # Default to DEVNULL to discard console output
|
212
|
+
) -> None:
|
128
213
|
"""Initialize the ServerLogger."""
|
129
|
-
self.
|
130
|
-
self.
|
214
|
+
self.host: str = host
|
215
|
+
self.port: int = port
|
216
|
+
self.server_url: str = server_url or f"{self.host}:{self.port}"
|
217
|
+
self.level: LogLevel = LogLevel.get(level)
|
131
218
|
self.client: AsyncClient = AsyncClient(timeout=5.0)
|
132
|
-
self.
|
219
|
+
self.file: T = file
|
220
|
+
|
221
|
+
async def post(self, url: str, json: dict) -> "Response":
|
222
|
+
"""Send a POST request to the server."""
|
223
|
+
return await self.client.post(url=url, json=json)
|
133
224
|
|
134
|
-
async def _log(self,
|
225
|
+
async def _log(self, request: LogRequest) -> None:
|
135
226
|
"""Same interface as SimpleLogger._log but calls HTTP endpoint."""
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
self._fallback_log(lvl, msg, *args, **kwargs)
|
149
|
-
except Exception:
|
150
|
-
self._fallback_log(lvl, msg, *args, **kwargs)
|
227
|
+
try:
|
228
|
+
response: Response = await self.post(url=f"{self.server_url}/log", json=request.model_dump())
|
229
|
+
if response.status_code != HTTPStatusCode.SERVER_OK:
|
230
|
+
await self._fallback_log(str(request.level), request.message, *request.args, **request.kwargs)
|
231
|
+
except Exception:
|
232
|
+
await self._fallback_log(str(request.level), request.message, *request.args, **request.kwargs)
|
233
|
+
|
234
|
+
async def log(self, level: LogLevel, msg: object, *args: Any, **kwargs: Any) -> None:
|
235
|
+
"""Log a message at the specified level."""
|
236
|
+
if level.value >= self.level.value:
|
237
|
+
request = LogRequest(level=level.value, message=str(msg), args=args, kwargs=kwargs)
|
238
|
+
await self._log(request)
|
151
239
|
|
152
|
-
def _fallback_log(self, lvl:
|
240
|
+
async def _fallback_log(self, lvl: str, msg: object, *args: Any, **kwargs: Any) -> None:
|
153
241
|
"""Fallback - same as original SimpleLogger._log."""
|
154
242
|
timestamp: str = EpochTimestamp.now().to_string()
|
155
|
-
print(f"[{timestamp}] {lvl
|
243
|
+
print(f"Fallback Logging: [{timestamp}] {lvl}: {msg}", file=self.file)
|
156
244
|
if args:
|
157
|
-
print(" ".join(str(arg) for arg in args), file=
|
245
|
+
print(" ".join(str(arg) for arg in args), file=self.file)
|
158
246
|
if kwargs:
|
159
247
|
for key, value in kwargs.items():
|
160
|
-
print(f"{key}={value}", file=
|
248
|
+
print(f"{key}={value}", file=self.file)
|
161
249
|
|
162
250
|
async def verbose(self, msg: object, *args, **kwargs) -> None:
|
163
251
|
"""Log a verbose message."""
|
164
|
-
await self.
|
252
|
+
await self.log(VERBOSE, msg, *args, **kwargs)
|
165
253
|
|
166
254
|
async def debug(self, msg: object, *args, **kwargs) -> None:
|
167
255
|
"""Log a debug message."""
|
168
|
-
await self.
|
256
|
+
await self.log(DEBUG, msg, *args, **kwargs)
|
169
257
|
|
170
258
|
async def info(self, msg: object, *args, **kwargs) -> None:
|
171
259
|
"""Log an info message."""
|
172
|
-
await self.
|
260
|
+
await self.log(INFO, msg, *args, **kwargs)
|
173
261
|
|
174
262
|
async def warning(self, msg: object, *args, **kwargs) -> None:
|
175
263
|
"""Log a warning message."""
|
176
|
-
await self.
|
264
|
+
await self.log(WARNING, msg, *args, **kwargs)
|
177
265
|
|
178
266
|
async def error(self, msg: object, *args, **kwargs) -> None:
|
179
267
|
"""Log an error message."""
|
180
|
-
await self.
|
268
|
+
await self.log(ERROR, msg, *args, **kwargs)
|
269
|
+
|
270
|
+
async def failure(self, msg: object, *args, **kwargs) -> None:
|
271
|
+
"""Log a failure message."""
|
272
|
+
await self.log(FAILURE, msg, *args, **kwargs)
|
273
|
+
|
274
|
+
async def success(self, msg: object, *args, **kwargs) -> None:
|
275
|
+
"""Log a success message."""
|
276
|
+
await self.log(SUCCESS, msg, *args, **kwargs)
|
181
277
|
|
182
278
|
async def close(self) -> None:
|
183
279
|
"""Close the HTTP client."""
|
184
280
|
await self.client.aclose()
|
185
281
|
|
186
|
-
|
187
|
-
|
188
|
-
|
282
|
+
async def __aenter__(self) -> Self:
|
283
|
+
"""Enter the asynchronous context manager."""
|
284
|
+
return self
|
285
|
+
|
286
|
+
async def __aexit__(self, exc_type: object, exc_value: object, traceback: object) -> None:
|
287
|
+
"""Exit the asynchronous context manager."""
|
288
|
+
await self.close()
|
289
|
+
|
290
|
+
|
291
|
+
async def run_server(
|
292
|
+
host: str = "localhost",
|
293
|
+
port: int = 8080,
|
294
|
+
log_file: str = "server.log",
|
295
|
+
level: LogLevel | int | str = DEBUG,
|
296
|
+
file: TextIO = DEVNULL,
|
297
|
+
) -> ExitCode | int:
|
298
|
+
"""Run the local logging server."""
|
299
|
+
server: LoggingServer[TextIO] = LoggingServer(
|
300
|
+
host=host,
|
301
|
+
port=port,
|
302
|
+
log_file=log_file,
|
303
|
+
level=level,
|
304
|
+
file=file,
|
305
|
+
)
|
189
306
|
try:
|
190
307
|
while True:
|
191
|
-
server.start()
|
308
|
+
await server.start()
|
192
309
|
except KeyboardInterrupt:
|
193
|
-
print("Stopping server...")
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
310
|
+
print("Stopping server...", file=server.file)
|
311
|
+
except Exception as e:
|
312
|
+
print(f"An error occurred: {e}", file=server.file)
|
313
|
+
return ExitCode.FAILURE
|
314
|
+
finally:
|
315
|
+
await server.stop()
|
316
|
+
return ExitCode.SUCCESS
|
317
|
+
|
318
|
+
|
319
|
+
def sync_server(
|
320
|
+
host: str = "localhost",
|
321
|
+
port: int = 8080,
|
322
|
+
log_file: str = "server.log",
|
323
|
+
level: LogLevel | int | str = DEBUG,
|
324
|
+
file: TextIO = DEVNULL,
|
325
|
+
) -> ExitCode | int:
|
326
|
+
"""Run the local logging server synchronously."""
|
327
|
+
loop = asyncio.get_event_loop()
|
328
|
+
asyncio.set_event_loop(loop)
|
329
|
+
return loop.run_until_complete(run_server(host, port, log_file, level, file))
|
330
|
+
|
331
|
+
|
332
|
+
# if __name__ == "__main__":
|
333
|
+
# exit_code = sync_server()
|
334
|
+
# sys.exit(exit_code)
|
@@ -1,30 +1,36 @@
|
|
1
1
|
"""Simple logger implementation with log levels and timestamped output."""
|
2
2
|
|
3
|
-
import
|
3
|
+
from io import StringIO
|
4
4
|
from typing import TextIO
|
5
5
|
|
6
|
-
from bear_utils.
|
6
|
+
from bear_utils.constants import STDERR, STDOUT
|
7
|
+
from bear_utils.logger_manager._log_level import LogLevel
|
7
8
|
from bear_utils.time import EpochTimestamp
|
8
9
|
|
9
|
-
|
10
|
-
|
10
|
+
VERBOSE: LogLevel = LogLevel.get("VERBOSE")
|
11
|
+
DEBUG: LogLevel = LogLevel.get("DEBUG")
|
12
|
+
INFO: LogLevel = LogLevel.get("INFO")
|
13
|
+
WARNING: LogLevel = LogLevel.get("WARNING")
|
14
|
+
ERROR: LogLevel = LogLevel.get("ERROR")
|
15
|
+
SUCCESS: LogLevel = LogLevel.get("SUCCESS")
|
16
|
+
FAILURE: LogLevel = LogLevel.get("FAILURE")
|
11
17
|
|
12
|
-
|
13
|
-
DEBUG: LogLevel = log_levels.get("DEBUG")
|
14
|
-
INFO: LogLevel = log_levels.get("INFO")
|
15
|
-
WARNING: LogLevel = log_levels.get("WARNING")
|
16
|
-
ERROR: LogLevel = log_levels.get("ERROR")
|
18
|
+
CHOICES = [STDOUT, STDERR, StringIO]
|
17
19
|
|
18
20
|
|
19
|
-
class SimpleLogger:
|
20
|
-
"""A simple logger that writes messages to stderr
|
21
|
+
class SimpleLogger[T: TextIO]:
|
22
|
+
"""A simple logger that writes messages to stdout, stderr, or StringIO with a timestamp."""
|
21
23
|
|
22
|
-
def __init__(self,
|
23
|
-
"""Initialize the logger with a minimum log level."""
|
24
|
-
self.
|
25
|
-
self.
|
24
|
+
def __init__(self, level: str | int | LogLevel = "DEBUG", file: T = STDERR) -> None:
|
25
|
+
"""Initialize the logger with a minimum log level and output file."""
|
26
|
+
self.level: LogLevel = LogLevel.get(level)
|
27
|
+
self.file: T = file
|
26
28
|
self.buffer: list[str] = []
|
27
29
|
|
30
|
+
def print(self, msg: object, end: str = "\n") -> None:
|
31
|
+
"""Print the message to the specified file with an optional end character."""
|
32
|
+
print(msg, end=end, file=self.file)
|
33
|
+
|
28
34
|
def _log(self, level: LogLevel, msg: object, end: str = "\n", *args, **kwargs) -> None:
|
29
35
|
timestamp: str = EpochTimestamp.now().to_string()
|
30
36
|
try:
|
@@ -34,15 +40,15 @@ class SimpleLogger:
|
|
34
40
|
if kwargs:
|
35
41
|
for key, value in kwargs.items():
|
36
42
|
self.buffer.append(f"{key}={value}")
|
37
|
-
print(f"{end}".join(self.buffer)
|
43
|
+
self.print(f"{end}".join(self.buffer))
|
38
44
|
except Exception as e:
|
39
|
-
print(f"[{timestamp}] {level.value}: {msg} - Error: {e}"
|
45
|
+
self.print(f"[{timestamp}] {level.value}: {msg} - Error: {e}")
|
40
46
|
finally:
|
41
47
|
self.buffer.clear()
|
42
48
|
|
43
49
|
def log(self, level: LogLevel, msg: object, *args, **kwargs) -> None:
|
44
50
|
"""Log a message at the specified level."""
|
45
|
-
if level.value >= self.
|
51
|
+
if level.value >= self.level.value:
|
46
52
|
self._log(level, msg, *args, **kwargs)
|
47
53
|
|
48
54
|
def verbose(self, msg: object, *args, **kwargs) -> None:
|
@@ -65,13 +71,22 @@ class SimpleLogger:
|
|
65
71
|
"""Log an error message."""
|
66
72
|
self.log(ERROR, msg, *args, **kwargs)
|
67
73
|
|
74
|
+
def success(self, msg: object, *args, **kwargs) -> None:
|
75
|
+
"""Log a success message."""
|
76
|
+
self.log(SUCCESS, msg, *args, **kwargs)
|
77
|
+
|
78
|
+
def failure(self, msg: object, *args, **kwargs) -> None:
|
79
|
+
"""Log a failure message."""
|
80
|
+
self.log(FAILURE, msg, *args, **kwargs)
|
81
|
+
|
68
82
|
|
69
83
|
# Example usage:
|
70
84
|
if __name__ == "__main__":
|
71
|
-
logger = SimpleLogger()
|
72
|
-
|
73
|
-
logger.verbose(msg="This is a verbose message")
|
74
|
-
logger.debug(msg="This is a debug message")
|
85
|
+
logger = SimpleLogger(file=StringIO())
|
86
|
+
logger_two = SimpleLogger(level="INFO", file=STDERR)
|
75
87
|
logger.info(msg="This is an info message")
|
76
|
-
|
77
|
-
|
88
|
+
logger_two.info(msg="This is an info message")
|
89
|
+
|
90
|
+
value = logger.file
|
91
|
+
print(value.getvalue()) # Print the captured log messages from StringIO
|
92
|
+
# print(logger_two.file.getvalue()) # should throw a typing error since it is not a StringIO
|
@@ -1,10 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bear-utils
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.9.0
|
4
4
|
Summary: Various utilities for Bear programmers, including a rich logging utility, a disk cache, and a SQLite database wrapper amongst other things.
|
5
5
|
Author-email: chaz <bright.lid5647@fastmail.com>
|
6
6
|
Requires-Python: >=3.12
|
7
|
-
Requires-Dist: bear-epoch-time>=1.1.
|
7
|
+
Requires-Dist: bear-epoch-time>=1.1.4
|
8
8
|
Requires-Dist: diskcache<6.0.0,>=5.6.3
|
9
9
|
Requires-Dist: fastapi>=0.116.0
|
10
10
|
Requires-Dist: httpx>=0.28.1
|
@@ -25,7 +25,7 @@ Provides-Extra: gui
|
|
25
25
|
Requires-Dist: pyqt6>=6.9.0; extra == 'gui'
|
26
26
|
Description-Content-Type: text/markdown
|
27
27
|
|
28
|
-
# Bear Utils v# Bear Utils v0.
|
28
|
+
# Bear Utils v# Bear Utils v0.9.0
|
29
29
|
|
30
30
|
Personal set of tools and utilities for Python projects, focusing on modularity and ease of use. This library includes components for caching, database management, logging, time handling, file operations, CLI prompts, image processing, clipboard interaction, gradient utilities, event systems, and async helpers.
|
31
31
|
|