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 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,4 @@
1
+ """Config package: default log instance and error handler factory."""
2
+ from ezlog_py.config.logger import create_error_handler, log
3
+
4
+ __all__ = ["log", "create_error_handler"]
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.27
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ ezlog-py = ezlog_py:main
3
+