ezpy_logs 0.2.0__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,11 @@
1
+ .envrc
2
+
3
+ .logs
4
+ __pycache__
5
+ .venv
6
+ .pytest_cache
7
+ dist
8
+
9
+ *.pyc
10
+ *.pyo
11
+ *.log
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 (unreleased)
4
+
5
+ New API: `ezpy_logs.setup(...)` once from the application, `ezpy_logs.get_logger(name)` everywhere.
6
+
7
+ Behaviour changes:
8
+
9
+ - **Files are written only after `setup()`.** `get_logger` never configures anything or creates
10
+ files. `LoggerFactory` keeps the old implicit setup in `./.logs` until 0.3, with a one-time
11
+ `DeprecationWarning`.
12
+ - **`setup()` replaces a `LoggerFactory` setup**; a second `setup()` with different arguments raises
13
+ instead of being silently ignored.
14
+ - **Handlers live on the root logger only**, and named loggers propagate. Third-party records now
15
+ reach the files; each record is written once.
16
+ - **The root level defaults to WARNING** (`level=` / `EZPY_LOGS_LEVEL`). Your own loggers go to
17
+ DEBUG through `app_loggers=[...]`. `LoggerFactory.getLogger(name)` still sets `name` to DEBUG.
18
+ - **UTC by default**: file names `2026-09-28T21-31-28Z_<pid>.log`, lines `2026-09-28T21:31:28.412Z`.
19
+ `tz="local"` keeps the machine's zone with its offset. (0.1 meant Paris time but used an invalid
20
+ zone name, so it was machine-local time.)
21
+ - **The pid is in archive file names**: parallel workers no longer share a file.
22
+ - **No colour when the stream is not a terminal**, or when `NO_COLOR` is set.
23
+ - **stdout takes every level below WARNING**, so custom levels such as SUCCESS=25 reach the terminal.
24
+ - **Old-log deletion is opt-in** (`retention_days=None` by default) and only touches `*.log` /
25
+ `*.jsonl` directly inside `<log_dir>/archive/` and `<log_dir>/archive_ERRORS/`: never symlinks,
26
+ never the current run's files, never anything else. It logs the count (INFO when non-zero). `LoggerFactory` keeps 0.1's 30-day retention, now with that narrower scope.
27
+ - **`Latest.log` / `Latest_ERRORS.log` are written on every run** (`latest=True`), not only under
28
+ pytest; off by default with `run_id`.
29
+ - `TqdmToLogger.flush` no longer logs empty lines.
30
+
31
+ New:
32
+
33
+ - JSON-lines sink (`json=True` / `EZPY_LOGS_JSON=1`).
34
+ - Run-scoped files: `setup(run_id="job-42")` writes `<log_dir>/job-42.log` and `.jsonl`.
35
+ - Best-effort secret redaction in every sink, plus `redact_values=[...]`. Key/value pairs are
36
+ caught under prefixed names (`GITHUB_TOKEN=`, `client_secret:`, `AWS_SECRET_ACCESS_KEY=`) and
37
+ quoted values with spaces; JSON extras under a sensitive key (`extra={"password": ...}`) are
38
+ redacted whole. `token_count=512` is left alone.
39
+ - Env configuration: `EZPY_LOGS_DIR`, `EZPY_LOGS_LEVEL`, `EZPY_LOGS_JSON`, `EZPY_LOGS_TZ`, `NO_COLOR`.
40
+
41
+ Packaging: no runtime dependencies (`colorama`, `python-dateutil` and `pytest` removed; pytest is a
42
+ dev dependency); ships `py.typed`; `hello.py` is no longer packaged.
43
+
44
+ Removed from `LoggerFactory`: `add_output_file`, `setup_logger` and the `replaced` attribute.
45
+ `is_setup`, `base_logging_directory` and `output_files` are now read-only views of the live setup.
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.5
2
+ Name: ezpy_logs
3
+ Version: 0.2.0
4
+ Summary: EzPy Logs: swiss knife logging for personal use
5
+ Project-URL: Homepage, https://github.com/ezalos/ezpy_logs
6
+ Project-URL: Repository, https://github.com/ezalos/ezpy_logs
7
+ Author-email: ezalos <ezalos@github.com>
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: System :: Logging
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+
19
+ # EZPY Logs
20
+
21
+ Package for my personal use, swiss knife logging :
22
+ - When logging, has a nice formatter with file line and datetime (ISO 8601, UTC by default)
23
+ - Colorful logger (only on a terminal; `NO_COLOR` turns it off)
24
+ - Easy import / use: `setup()` once, `get_logger()` everywhere
25
+ - Save to files, with errors, and optionally JSON lines for machines
26
+ - Best-effort secret redaction in every sink
27
+ - Opt-in deletion of its own old logs
28
+
29
+ No runtime dependencies.
30
+
31
+ ## Install
32
+
33
+ ```
34
+ uv add ezpy-logs
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```py
40
+ import ezpy_logs
41
+
42
+ logger = ezpy_logs.get_logger(__name__) # safe at module level: creates nothing
43
+
44
+ def main():
45
+ ezpy_logs.setup(app_loggers=["my_package"]) # once, from the application
46
+ logger.info("Hello from ezpy-logs!")
47
+
48
+ if __name__ == "__main__":
49
+ main()
50
+ ```
51
+
52
+ ```sh
53
+ uv run python hello.py
54
+ ```
55
+
56
+ Library code only calls `get_logger`. Nothing is configured and no file is written until
57
+ the application calls `setup()`.
58
+
59
+ ### `setup()` arguments
60
+
61
+ | Argument | Default | Meaning |
62
+ |---|---|---|
63
+ | `log_dir` | `$EZPY_LOGS_DIR`, else `.logs` | Where files go. |
64
+ | `level` | `$EZPY_LOGS_LEVEL`, else `WARNING` | Root level: every logger you don't own (urllib3, botocore...). |
65
+ | `app_loggers` | `()` | Your own logger names (usually your package); set to `app_level`. |
66
+ | `app_level` | `DEBUG` | Level of `app_loggers`. |
67
+ | `run_id` | `None` | Write `<log_dir>/<run_id>.log` (+ `.jsonl`) instead of timestamped archives. |
68
+ | `json` | `$EZPY_LOGS_JSON == "1"` | Also write JSON lines. |
69
+ | `tz` | `$EZPY_LOGS_TZ`, else `"utc"` | `"utc"` or `"local"` (with offset) for file names and text lines. JSON `ts` is always UTC. |
70
+ | `latest` | on, off with `run_id` | Also write `Latest.log` / `Latest_ERRORS.log`, overwritten by each run. |
71
+ | `retention_days` | `None` (never delete) | Delete this library's own `archive*/*.log`/`*.jsonl` older than N days. |
72
+ | `redact_values` | `()` | Literal secrets to redact (values under 8 characters are ignored). |
73
+
74
+ Calling `setup()` again with the same arguments does nothing; with different arguments it
75
+ raises `RuntimeError`.
76
+
77
+ Environment variables: `EZPY_LOGS_DIR`, `EZPY_LOGS_LEVEL`, `EZPY_LOGS_JSON=1`,
78
+ `EZPY_LOGS_TZ`, `NO_COLOR`.
79
+
80
+ ### What each sink looks like
81
+
82
+ Terminal (DEBUG/INFO on stdout, WARNING and above on stderr):
83
+
84
+ ```
85
+ INFO : 2026-09-28T21:46:49.393Z 14 ms [Thread 138181901391680] hello.py:12 || Hello from ezpy-logs!
86
+ WARNING : 2026-09-28T21:46:49.393Z 14 ms [Thread 138181901391680] hello.py:13 || token=[REDACTED:token] never reaches a sink
87
+ ```
88
+
89
+ Files, by default:
90
+
91
+ ```
92
+ .logs/archive/2026-09-28T21-46-49Z_1428568.log everything
93
+ .logs/archive/2026-09-28T21-46-49Z_1428568.jsonl everything, when json is on
94
+ .logs/archive_ERRORS/2026-09-28T21-46-49Z_1428568.log WARNING and above
95
+ .logs/Latest.log / Latest_ERRORS.log this run only
96
+ ```
97
+
98
+ A text file line:
99
+
100
+ ```
101
+ INFO 2026-09-28T21:46:49.393Z | [14] Thread 138181901391680 || [/path/hello.py:12] main || Hello from ezpy-logs!
102
+ ```
103
+
104
+ A JSON line (`extra={...}` fields are merged in; `exc` holds the traceback when there is one):
105
+
106
+ ```json
107
+ {"ts": "2026-09-28T21:46:49.393Z", "level": "WARNING", "logger": "__main__", "msg": "token=[REDACTED:token] never reaches a sink", "file": "/path/hello.py", "line": 13, "func": "main", "pid": 1428568, "thread": 138181901391680, "job": 7}
108
+ ```
109
+
110
+ ### Secret redaction is best effort
111
+
112
+ Every ezpy_logs sink redacts bearer tokens, `ya29.` Google OAuth tokens, `ghp_`/`github_pat_`,
113
+ `hf_`, `sk-`/`sk-ant-`, `AKIA` AWS keys, PEM private-key blocks, `password=`/`token=`/`secret=`
114
+ values, and the literal `redact_values`. Regexes cannot catch every secret, and a handler someone
115
+ else adds (Sentry, pytest's caplog) bypasses it: do not rely on it as a security boundary.
116
+
117
+ ### With tqdm
118
+
119
+ ```py
120
+ from tqdm import tqdm
121
+ import ezpy_logs
122
+
123
+ logger = ezpy_logs.get_logger(__name__)
124
+ for name in tqdm(files, file=ezpy_logs.TqdmToLogger(logger), miniters=1e4, maxinterval=float("inf")):
125
+ ...
126
+ ```
127
+
128
+ ### Migrating from 0.1
129
+
130
+ `from ezpy_logs.LoggerFactory import LoggerFactory` still works until 0.3, including its
131
+ implicit setup in `./.logs` (it emits a `DeprecationWarning` once). A `setup()` call always
132
+ replaces a `LoggerFactory` setup. See `CHANGELOG.md` for every behaviour change.
133
+
134
+ ## Dev
135
+
136
+ ### Local env setup
137
+
138
+ ```sh
139
+ uv sync
140
+ source .venv/bin/activate
141
+ ```
142
+
143
+ ### TESTING
144
+
145
+ ```sh
146
+ uv run pytest -v
147
+ ```
148
+
149
+ ### Publishing
150
+
151
+ Change version in `pyproject.toml`, add a `CHANGELOG.md` entry. `UV_PUBLISH_TOKEN` in `.envrc`
152
+ is a `pass://` ref (Agent vault, item `PIPY_TOKEN`), so publish through `secrets run`:
153
+
154
+ ```sh
155
+ rip dist
156
+ uv build && secrets run -- uv publish
157
+ ```
@@ -0,0 +1,139 @@
1
+ # EZPY Logs
2
+
3
+ Package for my personal use, swiss knife logging :
4
+ - When logging, has a nice formatter with file line and datetime (ISO 8601, UTC by default)
5
+ - Colorful logger (only on a terminal; `NO_COLOR` turns it off)
6
+ - Easy import / use: `setup()` once, `get_logger()` everywhere
7
+ - Save to files, with errors, and optionally JSON lines for machines
8
+ - Best-effort secret redaction in every sink
9
+ - Opt-in deletion of its own old logs
10
+
11
+ No runtime dependencies.
12
+
13
+ ## Install
14
+
15
+ ```
16
+ uv add ezpy-logs
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```py
22
+ import ezpy_logs
23
+
24
+ logger = ezpy_logs.get_logger(__name__) # safe at module level: creates nothing
25
+
26
+ def main():
27
+ ezpy_logs.setup(app_loggers=["my_package"]) # once, from the application
28
+ logger.info("Hello from ezpy-logs!")
29
+
30
+ if __name__ == "__main__":
31
+ main()
32
+ ```
33
+
34
+ ```sh
35
+ uv run python hello.py
36
+ ```
37
+
38
+ Library code only calls `get_logger`. Nothing is configured and no file is written until
39
+ the application calls `setup()`.
40
+
41
+ ### `setup()` arguments
42
+
43
+ | Argument | Default | Meaning |
44
+ |---|---|---|
45
+ | `log_dir` | `$EZPY_LOGS_DIR`, else `.logs` | Where files go. |
46
+ | `level` | `$EZPY_LOGS_LEVEL`, else `WARNING` | Root level: every logger you don't own (urllib3, botocore...). |
47
+ | `app_loggers` | `()` | Your own logger names (usually your package); set to `app_level`. |
48
+ | `app_level` | `DEBUG` | Level of `app_loggers`. |
49
+ | `run_id` | `None` | Write `<log_dir>/<run_id>.log` (+ `.jsonl`) instead of timestamped archives. |
50
+ | `json` | `$EZPY_LOGS_JSON == "1"` | Also write JSON lines. |
51
+ | `tz` | `$EZPY_LOGS_TZ`, else `"utc"` | `"utc"` or `"local"` (with offset) for file names and text lines. JSON `ts` is always UTC. |
52
+ | `latest` | on, off with `run_id` | Also write `Latest.log` / `Latest_ERRORS.log`, overwritten by each run. |
53
+ | `retention_days` | `None` (never delete) | Delete this library's own `archive*/*.log`/`*.jsonl` older than N days. |
54
+ | `redact_values` | `()` | Literal secrets to redact (values under 8 characters are ignored). |
55
+
56
+ Calling `setup()` again with the same arguments does nothing; with different arguments it
57
+ raises `RuntimeError`.
58
+
59
+ Environment variables: `EZPY_LOGS_DIR`, `EZPY_LOGS_LEVEL`, `EZPY_LOGS_JSON=1`,
60
+ `EZPY_LOGS_TZ`, `NO_COLOR`.
61
+
62
+ ### What each sink looks like
63
+
64
+ Terminal (DEBUG/INFO on stdout, WARNING and above on stderr):
65
+
66
+ ```
67
+ INFO : 2026-09-28T21:46:49.393Z 14 ms [Thread 138181901391680] hello.py:12 || Hello from ezpy-logs!
68
+ WARNING : 2026-09-28T21:46:49.393Z 14 ms [Thread 138181901391680] hello.py:13 || token=[REDACTED:token] never reaches a sink
69
+ ```
70
+
71
+ Files, by default:
72
+
73
+ ```
74
+ .logs/archive/2026-09-28T21-46-49Z_1428568.log everything
75
+ .logs/archive/2026-09-28T21-46-49Z_1428568.jsonl everything, when json is on
76
+ .logs/archive_ERRORS/2026-09-28T21-46-49Z_1428568.log WARNING and above
77
+ .logs/Latest.log / Latest_ERRORS.log this run only
78
+ ```
79
+
80
+ A text file line:
81
+
82
+ ```
83
+ INFO 2026-09-28T21:46:49.393Z | [14] Thread 138181901391680 || [/path/hello.py:12] main || Hello from ezpy-logs!
84
+ ```
85
+
86
+ A JSON line (`extra={...}` fields are merged in; `exc` holds the traceback when there is one):
87
+
88
+ ```json
89
+ {"ts": "2026-09-28T21:46:49.393Z", "level": "WARNING", "logger": "__main__", "msg": "token=[REDACTED:token] never reaches a sink", "file": "/path/hello.py", "line": 13, "func": "main", "pid": 1428568, "thread": 138181901391680, "job": 7}
90
+ ```
91
+
92
+ ### Secret redaction is best effort
93
+
94
+ Every ezpy_logs sink redacts bearer tokens, `ya29.` Google OAuth tokens, `ghp_`/`github_pat_`,
95
+ `hf_`, `sk-`/`sk-ant-`, `AKIA` AWS keys, PEM private-key blocks, `password=`/`token=`/`secret=`
96
+ values, and the literal `redact_values`. Regexes cannot catch every secret, and a handler someone
97
+ else adds (Sentry, pytest's caplog) bypasses it: do not rely on it as a security boundary.
98
+
99
+ ### With tqdm
100
+
101
+ ```py
102
+ from tqdm import tqdm
103
+ import ezpy_logs
104
+
105
+ logger = ezpy_logs.get_logger(__name__)
106
+ for name in tqdm(files, file=ezpy_logs.TqdmToLogger(logger), miniters=1e4, maxinterval=float("inf")):
107
+ ...
108
+ ```
109
+
110
+ ### Migrating from 0.1
111
+
112
+ `from ezpy_logs.LoggerFactory import LoggerFactory` still works until 0.3, including its
113
+ implicit setup in `./.logs` (it emits a `DeprecationWarning` once). A `setup()` call always
114
+ replaces a `LoggerFactory` setup. See `CHANGELOG.md` for every behaviour change.
115
+
116
+ ## Dev
117
+
118
+ ### Local env setup
119
+
120
+ ```sh
121
+ uv sync
122
+ source .venv/bin/activate
123
+ ```
124
+
125
+ ### TESTING
126
+
127
+ ```sh
128
+ uv run pytest -v
129
+ ```
130
+
131
+ ### Publishing
132
+
133
+ Change version in `pyproject.toml`, add a `CHANGELOG.md` entry. `UV_PUBLISH_TOKEN` in `.envrc`
134
+ is a `pass://` ref (Agent vault, item `PIPY_TOKEN`), so publish through `secrets run`:
135
+
136
+ ```sh
137
+ rip dist
138
+ uv build && secrets run -- uv publish
139
+ ```
@@ -0,0 +1,108 @@
1
+ # ABOUTME: Compatibility shim for the 0.1 API (LoggerFactory.getLogger / setup_LoggerFactory).
2
+ # ABOUTME: Keeps the old implicit setup until 0.3; a new-style ezpy_logs.setup() always replaces it.
3
+ import logging
4
+ import typing
5
+ import warnings
6
+
7
+ from ezpy_logs._core import (
8
+ DEFAULT_LOG_DIR,
9
+ LevelFilter,
10
+ TqdmToLogger,
11
+ _configure,
12
+ _resolve,
13
+ current_files,
14
+ delete_old_logs,
15
+ )
16
+ from ezpy_logs import _core
17
+
18
+ __all__ = ["BASE_LOG_DIR", "LevelFilter", "LoggerFactory", "TqdmToLogger", "remove_logs_older_than_n_days"]
19
+
20
+ MIN_LEVEL_FOR_LOGGERS = logging.DEBUG
21
+ BASE_LOG_DIR = DEFAULT_LOG_DIR
22
+ _LEGACY_RETENTION_DAYS = 30
23
+
24
+
25
+ def remove_logs_older_than_n_days(root_dir: str, n_days: int = 30) -> int:
26
+ return delete_old_logs(root_dir, n_days)
27
+
28
+
29
+ class _LegacyState(type):
30
+ """Class-level read-only views of the live setup, so they never go stale."""
31
+
32
+ @property
33
+ def is_setup(cls) -> bool:
34
+ return _core._state is not None
35
+
36
+ @property
37
+ def base_logging_directory(cls) -> str | None:
38
+ return _core._state.config.log_dir.as_posix() if _core._state else None
39
+
40
+ @property
41
+ def output_files(cls) -> list[tuple[str, int, int, str]]:
42
+ """(path, min_level, max_level, mode) of the files currently written, as in 0.1."""
43
+ return [(p.as_posix(), low, high, mode) for p, low, high, mode in current_files()]
44
+
45
+
46
+ class LoggerFactory(metaclass=_LegacyState):
47
+ """Deprecated: use `ezpy_logs.setup()` and `ezpy_logs.get_logger()`.
48
+
49
+ Legacy setups never fight a new-style one: `setup()` replaces them, and a legacy setup
50
+ after `setup()` is a no-op. Between themselves, the first legacy setup wins (0.1 behaviour).
51
+ """
52
+
53
+ setup_loggers: typing.ClassVar[list[str]] = []
54
+ _warned: typing.ClassVar[bool] = False
55
+
56
+ @classmethod
57
+ def setup_LoggerFactory(cls, log_dir: str = BASE_LOG_DIR, clean_old_logs: bool = True):
58
+ if _core._state is None:
59
+ config = _resolve(
60
+ log_dir=log_dir,
61
+ level=None,
62
+ app_loggers=(),
63
+ app_level=logging.DEBUG,
64
+ run_id=None,
65
+ json=None,
66
+ tz=None,
67
+ latest=None,
68
+ retention_days=_LEGACY_RETENTION_DAYS if clean_old_logs else None,
69
+ redact_values=(),
70
+ )
71
+ _configure(config, legacy=True)
72
+
73
+ @classmethod
74
+ def getLogger(
75
+ cls,
76
+ name: str = "should be __name__",
77
+ level=MIN_LEVEL_FOR_LOGGERS,
78
+ ) -> logging.Logger:
79
+ if _core._state is None:
80
+ if not cls._warned:
81
+ cls._warned = True
82
+ warnings.warn(
83
+ "LoggerFactory.getLogger() set up logging implicitly in ./.logs; this "
84
+ "implicit setup goes away in ezpy_logs 0.3. Call ezpy_logs.setup() "
85
+ "from your application and use ezpy_logs.get_logger().",
86
+ DeprecationWarning,
87
+ stacklevel=2,
88
+ )
89
+ cls.setup_LoggerFactory()
90
+ logger = logging.getLogger(name)
91
+ logger.setLevel(level)
92
+ if name not in cls.setup_loggers:
93
+ cls.setup_loggers.append(name)
94
+ return logger
95
+
96
+ @classmethod
97
+ def get_TqdmToLogger(cls, logger: logging.Logger, level=MIN_LEVEL_FOR_LOGGERS) -> TqdmToLogger:
98
+ return TqdmToLogger(logger, level)
99
+
100
+ @classmethod
101
+ def change_level_of_all_loggers(cls, level):
102
+ for name in cls.setup_loggers:
103
+ logging.getLogger(name).setLevel(level)
104
+
105
+ @classmethod
106
+ def _reset(cls) -> None:
107
+ cls.setup_loggers = []
108
+ cls._warned = False
@@ -0,0 +1,5 @@
1
+ # ABOUTME: Public API of ezpy_logs: setup() once from the application, get_logger() everywhere.
2
+ # ABOUTME: LoggerFactory stays importable from ezpy_logs.LoggerFactory as a deprecated alias until 0.3.
3
+ from ezpy_logs._core import TqdmToLogger, delete_old_logs, get_logger, is_setup, setup
4
+
5
+ __all__ = ["TqdmToLogger", "delete_old_logs", "get_logger", "is_setup", "setup"]