ezlog-py 1.0.1__tar.gz

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.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.3
2
+ Name: ezlog-py
3
+ Version: 1.0.1
4
+ Summary: Simple, performant logging with ANSI colors for Python.
5
+ Author: eaannist
6
+ Author-email: eaannist <eaannist@gmail.com>
7
+ Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
8
+ Requires-Python: >=3.12
9
+ Provides-Extra: dev
10
+ Description-Content-Type: text/markdown
11
+
12
+ # ezlog (Python)
13
+
14
+ Simple, performant logging with ANSI colors for Python.
15
+ Published on PyPI as **ezlog-py** (import as `ezlog`).
16
+
17
+ - **5 levels**: `error`, `warn`, `info`, `success`, `debug`
18
+ - **Short aliases**: `e`, `w`, `i`, `s`, `d`
19
+ - **ANSI colors**: red, yellow, cyan, green, magenta
20
+ - **Config**: levels on/off, colors, symbols vs text, timestamp
21
+ - **Safe serialization**: circular refs, dates, exceptions
22
+ - **Zero dependencies**
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install ezlog-py
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```python
33
+ from ezlog import EzLog, create_error_handler
34
+
35
+ # Create your own logger (no pre-initialized log instance)
36
+ logger = EzLog({
37
+ "levels": {"error": True, "warn": True, "info": True, "success": True, "debug": False},
38
+ "useColors": True,
39
+ "useLevels": True,
40
+ "useSymbols": False,
41
+ "useTimestamp": True,
42
+ })
43
+ logger.success("Application started")
44
+ logger.info("Environment: dev")
45
+ logger.w("Warning")
46
+ logger.e("Error")
47
+ logger.d("Debug")
48
+
49
+ logger.configure({"useTimestamp": False})
50
+ logger.info("User:", {"id": 1, "name": "John"})
51
+ logger.error("Failed", Exception("Connection failed"))
52
+
53
+ # Error handler for routers (e.g. FastAPI/Starlette)
54
+ on_error = create_error_handler()
55
+ on_error(exception, request)
56
+ ```
57
+
58
+ ## Config
59
+
60
+ | Option | Description |
61
+ |----------------|----------------------------------------|
62
+ | `levels` | Enable/disable per level |
63
+ | `useColors` | ANSI colors on/off |
64
+ | `useLevels` | Show level label before message |
65
+ | `useSymbols` | Use symbols (x, !, i, ✓, d) or text |
66
+ | `useTimestamp`| ISO timestamp before message |
67
+
68
+ ## Exports
69
+
70
+ - `EzLog` – logger class (create instances yourself)
71
+ - `create_error_handler(is_http_error=..., get_method=..., get_url=...)` – for router error callbacks
72
+ - Types: `LogLevel`, `LogColors`, `EzlogConfig`, `LevelsConfig`, `LevelConfig`, `LogArgs`, `ConsoleMethod`
@@ -0,0 +1,61 @@
1
+ # ezlog (Python)
2
+
3
+ Simple, performant logging with ANSI colors for Python.
4
+ Published on PyPI as **ezlog-py** (import as `ezlog`).
5
+
6
+ - **5 levels**: `error`, `warn`, `info`, `success`, `debug`
7
+ - **Short aliases**: `e`, `w`, `i`, `s`, `d`
8
+ - **ANSI colors**: red, yellow, cyan, green, magenta
9
+ - **Config**: levels on/off, colors, symbols vs text, timestamp
10
+ - **Safe serialization**: circular refs, dates, exceptions
11
+ - **Zero dependencies**
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install ezlog-py
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```python
22
+ from ezlog import EzLog, create_error_handler
23
+
24
+ # Create your own logger (no pre-initialized log instance)
25
+ logger = EzLog({
26
+ "levels": {"error": True, "warn": True, "info": True, "success": True, "debug": False},
27
+ "useColors": True,
28
+ "useLevels": True,
29
+ "useSymbols": False,
30
+ "useTimestamp": True,
31
+ })
32
+ logger.success("Application started")
33
+ logger.info("Environment: dev")
34
+ logger.w("Warning")
35
+ logger.e("Error")
36
+ logger.d("Debug")
37
+
38
+ logger.configure({"useTimestamp": False})
39
+ logger.info("User:", {"id": 1, "name": "John"})
40
+ logger.error("Failed", Exception("Connection failed"))
41
+
42
+ # Error handler for routers (e.g. FastAPI/Starlette)
43
+ on_error = create_error_handler()
44
+ on_error(exception, request)
45
+ ```
46
+
47
+ ## Config
48
+
49
+ | Option | Description |
50
+ |----------------|----------------------------------------|
51
+ | `levels` | Enable/disable per level |
52
+ | `useColors` | ANSI colors on/off |
53
+ | `useLevels` | Show level label before message |
54
+ | `useSymbols` | Use symbols (x, !, i, ✓, d) or text |
55
+ | `useTimestamp`| ISO timestamp before message |
56
+
57
+ ## Exports
58
+
59
+ - `EzLog` – logger class (create instances yourself)
60
+ - `create_error_handler(is_http_error=..., get_method=..., get_url=...)` – for router error callbacks
61
+ - Types: `LogLevel`, `LogColors`, `EzlogConfig`, `LevelsConfig`, `LevelConfig`, `LogArgs`, `ConsoleMethod`
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "ezlog-py"
3
+ version = "1.0.1"
4
+ description = "Simple, performant logging with ANSI colors for Python."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "eaannist", email = "eaannist@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = []
11
+
12
+ [project.optional-dependencies]
13
+ dev = ["pytest>=8.0.0"]
14
+
15
+ [project.scripts]
16
+ ezlog-py = "ezlog:main"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.9.27,<0.10.0"]
20
+ build-backend = "uv_build"
21
+
22
+ [tool.uv.build-backend]
23
+ module-root = "src"
24
+ module-name = "ezlog"
@@ -0,0 +1,54 @@
1
+ """
2
+ ezlog: simple, performant logging with ANSI colors (Python port of ezlog).
3
+ Install as ezlog-py from PyPI; import as: from ezlog import EzLog, create_error_handler.
4
+ 5 levels: error, warn, info, success, debug. Short aliases: e, w, i, s, d.
5
+ """
6
+ from ezlog.ezlog import EzLog, create_error_handler
7
+ from ezlog.types import (
8
+ ConsoleMethod,
9
+ EzlogConfig,
10
+ LevelConfig,
11
+ LevelsConfig,
12
+ LogArgs,
13
+ LogColors,
14
+ LogLevel,
15
+ )
16
+
17
+ __all__ = [
18
+ "EzLog",
19
+ "create_error_handler",
20
+ "LogLevel",
21
+ "LogColors",
22
+ "EzlogConfig",
23
+ "LevelsConfig",
24
+ "LevelConfig",
25
+ "LogArgs",
26
+ "ConsoleMethod",
27
+ ]
28
+
29
+
30
+ def main() -> None:
31
+ """CLI entry point: short demo (users create their own EzLog())."""
32
+ logger = EzLog(
33
+ {
34
+ "levels": {
35
+ "error": True,
36
+ "warn": True,
37
+ "info": True,
38
+ "success": True,
39
+ "debug": False,
40
+ },
41
+ "useColors": True,
42
+ "useLevels": True,
43
+ "useSymbols": False,
44
+ "useTimestamp": True,
45
+ }
46
+ )
47
+ logger.success("Application started")
48
+ logger.info("Environment: dev")
49
+ logger.w("Warning message")
50
+ logger.e("Error message")
51
+ logger.d("Debug message")
52
+ logger.info("User data:", {"id": 1, "name": "John"})
53
+ logger.configure({"useTimestamp": False})
54
+ logger.s("Done")
@@ -0,0 +1,419 @@
1
+ """
2
+ EzLog: simple, performant logging with ANSI colors.
3
+ Mirrors ezlog (TypeScript) API: 5 levels, short aliases, safe serialization.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ import re
10
+ import sys
11
+ import traceback
12
+ from datetime import datetime
13
+ from typing import Any, Callable
14
+
15
+ from ezlog.types import EzlogConfig, LevelConfig, LogLevel
16
+
17
+ # Compiled regex for stack line formatting (path:line:col).
18
+ _STACK_PATH_LINE_RE = re.compile(r"(\(?)([^\s()]+):(\d+):(\d+)(\)?)")
19
+ _STACK_FILE_RE = re.compile(r"^\s*File\s*")
20
+ _STACK_AT_RE = re.compile(r"^\s*at\s*")
21
+
22
+
23
+ def _safe_symbol(s: str, fallback: str = "?") -> str:
24
+ """Use fallback if default encoding cannot encode s (e.g. Windows cp1252 and ✓)."""
25
+ try:
26
+ enc = getattr(sys.stdout, "encoding", None) or sys.getdefaultencoding()
27
+ s.encode(enc)
28
+ return s
29
+ except (UnicodeEncodeError, AttributeError):
30
+ return fallback
31
+
32
+
33
+ _DEFAULT_CONFIG: EzlogConfig = {
34
+ "levels": {
35
+ "error": True,
36
+ "warn": True,
37
+ "info": True,
38
+ "success": True,
39
+ "debug": True,
40
+ },
41
+ "useColors": True,
42
+ "useLevels": True,
43
+ "useSymbols": True,
44
+ "useTimestamp": True,
45
+ }
46
+
47
+
48
+ def _merge_levels(
49
+ base: dict[str, bool], override: dict[str, bool] | None
50
+ ) -> dict[str, bool]:
51
+ out = dict(base)
52
+ if override:
53
+ out.update(override)
54
+ return out
55
+
56
+
57
+ class EzLog:
58
+ """
59
+ Simple, performant, type-safe logging with ANSI colors.
60
+ Levels: error, warn, info, success, debug. Short aliases: e, w, i, s, d.
61
+ """
62
+
63
+ def __init__(self, config: EzlogConfig | None = None) -> None:
64
+ cfg = config or {}
65
+ self._config: EzlogConfig = {
66
+ "levels": _merge_levels(
67
+ _DEFAULT_CONFIG.get("levels", {}), # type: ignore[arg-type]
68
+ cfg.get("levels"),
69
+ ),
70
+ "useColors": cfg.get("useColors", _DEFAULT_CONFIG.get("useColors", True)),
71
+ "useLevels": cfg.get("useLevels", _DEFAULT_CONFIG.get("useLevels", True)),
72
+ "useSymbols": cfg.get("useSymbols", _DEFAULT_CONFIG.get("useSymbols", True)),
73
+ "useTimestamp": cfg.get(
74
+ "useTimestamp", _DEFAULT_CONFIG.get("useTimestamp", True)
75
+ ),
76
+ }
77
+ self._colors = self._build_colors()
78
+ self._level_config = self._build_level_config()
79
+
80
+ def _build_colors(self) -> dict[str, str]:
81
+ use = self._config.get("useColors", True)
82
+ return {
83
+ "red": "\x1b[31m" if use else "",
84
+ "yellow": "\x1b[33m" if use else "",
85
+ "cyan": "\x1b[36m" if use else "",
86
+ "green": "\x1b[32m" if use else "",
87
+ "magenta": "\x1b[35m" if use else "",
88
+ "white": "\x1b[0m" if use else "",
89
+ }
90
+
91
+ def _build_level_config(self) -> dict[str, LevelConfig]:
92
+ c = self._colors
93
+ return {
94
+ "error": {
95
+ "symbol": "x",
96
+ "text": "ERROR",
97
+ "color": c["red"],
98
+ "consoleFn": lambda s: print(s, file=sys.stderr),
99
+ },
100
+ "warn": {
101
+ "symbol": "!",
102
+ "text": "WARN",
103
+ "color": c["yellow"],
104
+ "consoleFn": lambda s: print(s, file=sys.stderr),
105
+ },
106
+ "info": {
107
+ "symbol": "i",
108
+ "text": "INFO",
109
+ "color": c["cyan"],
110
+ "consoleFn": lambda s: print(s),
111
+ },
112
+ "success": {
113
+ "symbol": _safe_symbol("✓", "+"),
114
+ "text": "SUCCESS",
115
+ "color": c["green"],
116
+ "consoleFn": lambda s: print(s),
117
+ },
118
+ "debug": {
119
+ "symbol": "d",
120
+ "text": "DEBUG",
121
+ "color": c["magenta"],
122
+ "consoleFn": lambda s: print(s),
123
+ },
124
+ }
125
+
126
+ def configure(self, config: EzlogConfig) -> None:
127
+ """Update configuration at runtime."""
128
+ if "levels" in config and config["levels"]:
129
+ self._config["levels"] = _merge_levels(
130
+ self._config.get("levels") or {}, config["levels"]
131
+ )
132
+ for key in ("useColors", "useLevels", "useSymbols", "useTimestamp"):
133
+ if key in config and config[key] is not None:
134
+ self._config[key] = config[key] # type: ignore[typeddict-unknown-key]
135
+ if "useColors" in config:
136
+ self._colors = self._build_colors()
137
+ self._level_config = self._build_level_config()
138
+
139
+ def get_config(self) -> EzlogConfig:
140
+ """Return a copy of current config."""
141
+ return {
142
+ "levels": dict(self._config.get("levels") or {}),
143
+ "useColors": self._config.get("useColors", True),
144
+ "useLevels": self._config.get("useLevels", True),
145
+ "useSymbols": self._config.get("useSymbols", True),
146
+ "useTimestamp": self._config.get("useTimestamp", True),
147
+ }
148
+
149
+ def _get_timestamp(self, color: str) -> str:
150
+ if not self._config.get("useTimestamp", True):
151
+ return ""
152
+ now = datetime.now().isoformat()[:19].replace("T", " ")
153
+ return f"[{color}{now}{self._colors['white']}] "
154
+
155
+ def _get_prefix(self, level: LogLevel) -> str:
156
+ lc = self._level_config[level]
157
+ display = lc["symbol"] if self._config.get("useSymbols", True) else lc["text"]
158
+ ts = self._get_timestamp(lc["color"])
159
+ if self._config.get("useLevels", True):
160
+ return (
161
+ f"{self._colors['white']}{ts}"
162
+ f"[{lc['color']}{display}{self._colors['white']}] "
163
+ )
164
+ return f"{self._colors['white']}{ts}"
165
+
166
+ def _safe_stringify(self, obj: Any, space: int | None = None) -> str:
167
+ try:
168
+ cloned = self._safe_clone_for_logging(obj)
169
+ return json.dumps(cloned, indent=space, default=str)
170
+ except (TypeError, ValueError, RecursionError):
171
+ return "[Non-serializable object]"
172
+
173
+ def _safe_clone_for_logging(
174
+ self, obj: Any, depth: int = 0, seen: set[int] | None = None
175
+ ) -> Any:
176
+ if depth > 10:
177
+ return "[Max Depth Reached]"
178
+ seen = seen or set()
179
+ if obj is None:
180
+ return obj
181
+ if isinstance(obj, datetime):
182
+ return obj.isoformat()
183
+ if isinstance(obj, BaseException):
184
+ return {
185
+ "name": type(obj).__name__,
186
+ "message": str(obj),
187
+ "stack": (
188
+ str(obj.__traceback__)
189
+ if getattr(obj, "__traceback__", None)
190
+ else None
191
+ ),
192
+ }
193
+ if isinstance(obj, list):
194
+ if id(obj) in seen:
195
+ return "[Circular Reference]"
196
+ seen.add(id(obj))
197
+ try:
198
+ return [self._safe_clone_for_logging(x, depth + 1, seen) for x in obj]
199
+ finally:
200
+ seen.discard(id(obj))
201
+ if isinstance(obj, dict):
202
+ if id(obj) in seen:
203
+ return "[Circular Reference]"
204
+ seen.add(id(obj))
205
+ out: dict[str, Any] = {}
206
+ try:
207
+ for k, v in obj.items():
208
+ try:
209
+ out[k] = self._safe_clone_for_logging(v, depth + 1, seen)
210
+ except RecursionError:
211
+ out[k] = "[Circular Reference]"
212
+ return out
213
+ finally:
214
+ seen.discard(id(obj))
215
+ return obj
216
+
217
+ def _format_stack(self, stack: str) -> str:
218
+ if not stack:
219
+ return ""
220
+ lines = stack.strip().split("\n")
221
+ out_lines = []
222
+ for i, line in enumerate(lines):
223
+ trimmed = line.strip()
224
+ if i == 0:
225
+ out_lines.append(
226
+ f"{self._colors['white']}{self._colors['red']}"
227
+ f"{trimmed}{self._colors['white']}"
228
+ )
229
+ continue
230
+ cleaned = _STACK_FILE_RE.sub(" at ", trimmed)
231
+ cleaned = _STACK_AT_RE.sub(" at ", cleaned)
232
+ highlighted = _STACK_PATH_LINE_RE.sub(
233
+ lambda m: f"{m.group(1)}{self._colors['cyan']}{m.group(2)}"
234
+ f"{self._colors['white']}:{self._colors['magenta']}{m.group(3)}"
235
+ f"{self._colors['white']}:{self._colors['magenta']}{m.group(4)}"
236
+ f"{self._colors['white']}{m.group(5)}",
237
+ cleaned,
238
+ )
239
+ out_lines.append(
240
+ f" {self._colors['magenta']}at{self._colors['white']} {highlighted}"
241
+ )
242
+ return "\n".join(out_lines)
243
+
244
+ def _format_arg(self, arg: Any) -> str:
245
+ if isinstance(arg, BaseException):
246
+ parts = [
247
+ f"{self._colors['red']}{type(arg).__name__}"
248
+ f"{self._colors['white']}: {arg}"
249
+ ]
250
+ if getattr(arg, "code", None) is not None:
251
+ parts.append(
252
+ f"{self._colors['cyan']}code{self._colors['white']}: {arg.code}"
253
+ )
254
+ if getattr(arg, "status_code", None) is not None:
255
+ parts.append(
256
+ f"{self._colors['cyan']}statusCode{self._colors['white']}: "
257
+ f"{arg.status_code}"
258
+ )
259
+ if getattr(arg, "statusCode", None) is not None:
260
+ parts.append(
261
+ f"{self._colors['cyan']}statusCode{self._colors['white']}: "
262
+ f"{arg.statusCode}"
263
+ )
264
+ cause = getattr(arg, "__cause__", None)
265
+ if cause is not None:
266
+ cstr = (
267
+ f"{type(cause).__name__}: {cause}"
268
+ if isinstance(cause, BaseException)
269
+ else str(cause)
270
+ )
271
+ parts.append(
272
+ f"{self._colors['cyan']}cause{self._colors['white']}: {cstr}"
273
+ )
274
+ tb = getattr(arg, "__traceback__", None)
275
+ if tb is not None:
276
+ parts.append(
277
+ self._format_stack("".join(traceback.format_tb(tb)))
278
+ )
279
+ return "\n".join(parts) + "\n"
280
+ if isinstance(arg, (dict, list)):
281
+ try:
282
+ safe = self._safe_clone_for_logging(arg)
283
+ return self._safe_stringify(safe, 2) + "\n"
284
+ except (TypeError, ValueError, RecursionError):
285
+ return "[Non-serializable object]\n"
286
+ return str(arg)
287
+
288
+ def _format_args(self, *args: Any) -> str:
289
+ return " ".join(self._format_arg(a) for a in args)
290
+
291
+ def _log(self, level: LogLevel, *args: Any) -> None:
292
+ levels = self._config.get("levels") or {}
293
+ if not levels.get(level, True) or not args:
294
+ return
295
+ lc = self._level_config[level]
296
+ msg = f"{self._get_prefix(level)}{self._format_args(*args)}"
297
+ lc["consoleFn"](msg)
298
+
299
+ def error(self, *args: Any) -> None:
300
+ self._log("error", *args)
301
+
302
+ def e(self, *args: Any) -> None:
303
+ self._log("error", *args)
304
+
305
+ def warn(self, *args: Any) -> None:
306
+ self._log("warn", *args)
307
+
308
+ def w(self, *args: Any) -> None:
309
+ self._log("warn", *args)
310
+
311
+ def info(self, *args: Any) -> None:
312
+ self._log("info", *args)
313
+
314
+ def i(self, *args: Any) -> None:
315
+ self._log("info", *args)
316
+
317
+ def success(self, *args: Any) -> None:
318
+ self._log("success", *args)
319
+
320
+ def s(self, *args: Any) -> None:
321
+ self._log("success", *args)
322
+
323
+ def debug(self, *args: Any) -> None:
324
+ self._log("debug", *args)
325
+
326
+ def d(self, *args: Any) -> None:
327
+ self._log("debug", *args)
328
+
329
+ @property
330
+ def green(self) -> str:
331
+ return self._colors["green"]
332
+
333
+ @property
334
+ def red(self) -> str:
335
+ return self._colors["red"]
336
+
337
+ @property
338
+ def yellow(self) -> str:
339
+ return self._colors["yellow"]
340
+
341
+ @property
342
+ def cyan(self) -> str:
343
+ return self._colors["cyan"]
344
+
345
+ @property
346
+ def magenta(self) -> str:
347
+ return self._colors["magenta"]
348
+
349
+ @property
350
+ def white(self) -> str:
351
+ return self._colors["white"]
352
+
353
+
354
+ # --- create_error_handler (uses internal logger, not exported as "log") ---
355
+
356
+ _IS_PRODUCTION = os.environ.get("ENV", "").lower() == "production"
357
+
358
+ _log = EzLog(
359
+ {
360
+ "levels": {
361
+ "error": True,
362
+ "warn": True,
363
+ "info": True,
364
+ "success": True,
365
+ "debug": not _IS_PRODUCTION,
366
+ },
367
+ "useColors": True,
368
+ "useLevels": True,
369
+ "useSymbols": True,
370
+ "useTimestamp": True,
371
+ }
372
+ )
373
+
374
+
375
+ def _default_is_http_error(err: Any) -> bool:
376
+ """True if err has status_code or statusCode (common HTTP error pattern)."""
377
+ return hasattr(err, "status_code") or hasattr(err, "statusCode")
378
+
379
+
380
+ def _default_status_code(err: Any) -> int:
381
+ """Extract status code from error (status_code or statusCode)."""
382
+ return getattr(err, "status_code", None) or getattr(err, "statusCode", 500)
383
+
384
+
385
+ def create_error_handler(
386
+ *,
387
+ is_http_error: Callable[[Any], bool] | None = None,
388
+ get_method: Callable[[Any], str] | None = None,
389
+ get_url: Callable[[Any], str] | None = None,
390
+ ) -> Callable[[Any, Any], None]:
391
+ """
392
+ Create error handler for router on_error callback.
393
+ Logs by level: 5xx -> error, 4xx -> warn, else -> info.
394
+ """
395
+ is_http = is_http_error or _default_is_http_error
396
+ get_m = get_method or (
397
+ lambda req: getattr(req, "method", getattr(req, "METHOD", "?"))
398
+ )
399
+ get_u = get_url or (
400
+ lambda req: getattr(req, "url", getattr(req, "path", "?"))
401
+ )
402
+
403
+ def handler(err: Any, request: Any = None) -> None:
404
+ if request is None:
405
+ method, url = "?", "?"
406
+ else:
407
+ method, url = get_m(request), get_u(request)
408
+ if is_http(err):
409
+ code = _default_status_code(err)
410
+ if code >= 500:
411
+ _log.e(f"[{method}] {url} - {code}", err)
412
+ elif code >= 400:
413
+ _log.w(f"[{method}] {url} - {code}", err)
414
+ else:
415
+ _log.i(f"[{method}] {url} - {code}", err)
416
+ else:
417
+ _log.e(f"[{method}] {url} - Unhandled error", err)
418
+
419
+ return handler
@@ -0,0 +1,38 @@
1
+ """Type definitions for ezlog (mirrors ezlog TypeScript API)."""
2
+ from typing import Any, Callable, Literal, TypedDict
3
+
4
+ LogLevel = Literal["error", "warn", "info", "success", "debug"]
5
+ LogColors = Literal["red", "yellow", "cyan", "green", "magenta", "white"]
6
+
7
+
8
+ class LevelsConfig(TypedDict, total=False):
9
+ """Per-level enable/disable."""
10
+
11
+ error: bool
12
+ warn: bool
13
+ info: bool
14
+ success: bool
15
+ debug: bool
16
+
17
+
18
+ class EzlogConfig(TypedDict, total=False):
19
+ """Logger configuration (partial for updates)."""
20
+
21
+ levels: LevelsConfig
22
+ useColors: bool
23
+ useLevels: bool
24
+ useSymbols: bool
25
+ useTimestamp: bool
26
+
27
+
28
+ class LevelConfig(TypedDict):
29
+ """Internal config per level: symbol, text, color, writer."""
30
+
31
+ symbol: str
32
+ text: str
33
+ color: str
34
+ consoleFn: Callable[..., None]
35
+
36
+
37
+ ConsoleMethod = Callable[..., None]
38
+ LogArgs = tuple[Any, ...]