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