jacl4py 0.0.1__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.
- jacl4py/__init__.py +3 -0
- jacl4py/exceptions.py +5 -0
- jacl4py/logger.py +260 -0
- jacl4py-0.0.1.dist-info/METADATA +245 -0
- jacl4py-0.0.1.dist-info/RECORD +8 -0
- jacl4py-0.0.1.dist-info/WHEEL +4 -0
- jacl4py-0.0.1.dist-info/entry_points.txt +4 -0
- jacl4py-0.0.1.dist-info/licenses/LICENSE +28 -0
jacl4py/__init__.py
ADDED
jacl4py/exceptions.py
ADDED
jacl4py/logger.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging utilities: ``JacLogger`` subclass with project-specific levels and
|
|
3
|
+
a ``get_logger`` factory that attaches console + rotating file handlers.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import tempfile
|
|
9
|
+
import warnings
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from threading import RLock
|
|
15
|
+
from uuid import uuid4
|
|
16
|
+
|
|
17
|
+
import coloredlogs
|
|
18
|
+
|
|
19
|
+
from . import __version__ as _package_version
|
|
20
|
+
from .exceptions import ResourceNotInitializedError
|
|
21
|
+
|
|
22
|
+
__version__ = _package_version
|
|
23
|
+
|
|
24
|
+
INTERNAL_DEBUG = 5
|
|
25
|
+
INTERNAL_INFO = 7
|
|
26
|
+
logging.addLevelName(INTERNAL_DEBUG, "INTERNAL_DEBUG")
|
|
27
|
+
logging.addLevelName(INTERNAL_INFO, "INTERNAL_INFO")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LogDir(Enum):
|
|
31
|
+
"""Special log directory settings."""
|
|
32
|
+
|
|
33
|
+
DEFAULT = "default"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class JacLogger(logging.Logger):
|
|
37
|
+
"""Logger subclass that adds INTERNAL_INFO (7) and INTERNAL_DEBUG (5) levels below DEBUG."""
|
|
38
|
+
|
|
39
|
+
_deleted: bool = False
|
|
40
|
+
|
|
41
|
+
def isEnabledFor(self, level: int) -> bool:
|
|
42
|
+
"""Check the level, raising ResourceNotInitializedError after deletion."""
|
|
43
|
+
if self._deleted:
|
|
44
|
+
raise ResourceNotInitializedError(
|
|
45
|
+
f"Logger '{self.name}' has been deleted. "
|
|
46
|
+
"Obtain a new logger with get_logger() before logging."
|
|
47
|
+
)
|
|
48
|
+
return super().isEnabledFor(level)
|
|
49
|
+
|
|
50
|
+
def iinfo(self, message, *args, **kwargs):
|
|
51
|
+
"""Log at INTERNAL_INFO level (7) --- framework lifecycle events, below DEBUG."""
|
|
52
|
+
if self.isEnabledFor(INTERNAL_INFO):
|
|
53
|
+
self._log(INTERNAL_INFO, message, args, **kwargs)
|
|
54
|
+
|
|
55
|
+
def idebug(self, message, *args, **kwargs):
|
|
56
|
+
"""
|
|
57
|
+
Log at INTERNAL_DEBUG level (5) --- fine-grained framework tracing, below INTERNAL_INFO.
|
|
58
|
+
"""
|
|
59
|
+
if self.isEnabledFor(INTERNAL_DEBUG):
|
|
60
|
+
self._log(INTERNAL_DEBUG, message, args, **kwargs)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_manager = logging.Manager(logging.root)
|
|
64
|
+
_manager.setLoggerClass(JacLogger)
|
|
65
|
+
_lifecycle_lock = RLock()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _after_fork() -> None:
|
|
69
|
+
global _manager, _lifecycle_lock # pylint: disable=global-statement
|
|
70
|
+
for logger in _manager.loggerDict.values():
|
|
71
|
+
if isinstance(logger, JacLogger):
|
|
72
|
+
logger._deleted = True # pylint: disable=protected-access
|
|
73
|
+
logger.disabled = True
|
|
74
|
+
_manager = logging.Manager(logging.root)
|
|
75
|
+
_manager.setLoggerClass(JacLogger)
|
|
76
|
+
_lifecycle_lock = RLock()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if hasattr(os, "register_at_fork"):
|
|
80
|
+
os.register_at_fork(after_in_child=_after_fork)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _synchronized(function):
|
|
84
|
+
"""Serialize package logger lifecycle operations."""
|
|
85
|
+
@wraps(function)
|
|
86
|
+
def wrapped(*args, **kwargs):
|
|
87
|
+
"""Run the operation while holding the lifecycle lock."""
|
|
88
|
+
with _lifecycle_lock:
|
|
89
|
+
return function(*args, **kwargs)
|
|
90
|
+
return wrapped
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _create_log_path(name: str, log_dir: str | Path | LogDir) -> Path:
|
|
94
|
+
"""Reserve a unique sanitized log path without reusing an existing writer."""
|
|
95
|
+
safe_name = re.sub(r"[^A-Za-z0-9_.-]", "_", name).strip(".")[:64] or "logger"
|
|
96
|
+
if log_dir is not LogDir.DEFAULT:
|
|
97
|
+
directory = Path(log_dir)
|
|
98
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
while True:
|
|
100
|
+
identifier = safe_name + uuid4().hex[:8]
|
|
101
|
+
try:
|
|
102
|
+
if log_dir is LogDir.DEFAULT:
|
|
103
|
+
directory = Path(tempfile.gettempdir()) / identifier
|
|
104
|
+
directory.mkdir(mode=0o700)
|
|
105
|
+
return directory / f"{safe_name}.log"
|
|
106
|
+
filename = directory / f"{identifier}.log"
|
|
107
|
+
with filename.open("x", encoding="utf-8"):
|
|
108
|
+
pass
|
|
109
|
+
return filename
|
|
110
|
+
except FileExistsError:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@_synchronized
|
|
115
|
+
def delete_logger(name: str = "default") -> bool:
|
|
116
|
+
"""Close and unregister a package logger, keeping existing log files.
|
|
117
|
+
|
|
118
|
+
Return False if no named logger exists. Deleted JacLogger references
|
|
119
|
+
raise ResourceNotInitializedError when used to log;
|
|
120
|
+
package child loggers remain usable. Standard logging loggers are untouched.
|
|
121
|
+
"""
|
|
122
|
+
manager = _manager
|
|
123
|
+
logger = manager.loggerDict.get(name)
|
|
124
|
+
if not isinstance(logger, logging.Logger):
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
if isinstance(logger, JacLogger):
|
|
128
|
+
logger._deleted = True # pylint: disable=protected-access
|
|
129
|
+
logger.disabled = True
|
|
130
|
+
for handler in list(logger.handlers):
|
|
131
|
+
logger.removeHandler(handler)
|
|
132
|
+
handler.close()
|
|
133
|
+
del manager.loggerDict[name]
|
|
134
|
+
for child in list(manager.loggerDict.values()):
|
|
135
|
+
if isinstance(child, logging.Logger) and child.parent is logger:
|
|
136
|
+
manager._fixupParents(child) # pylint: disable=protected-access
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@_synchronized
|
|
141
|
+
def get_logger( # pylint: disable=too-many-arguments
|
|
142
|
+
name: str = "default",
|
|
143
|
+
*,
|
|
144
|
+
console_log_level: int | None = logging.INFO,
|
|
145
|
+
log_dir: str | Path | LogDir | None = LogDir.DEFAULT,
|
|
146
|
+
file_log_level: int | None = INTERNAL_DEBUG,
|
|
147
|
+
use_colored_output: bool = True,
|
|
148
|
+
reinit_if_exists: bool = False
|
|
149
|
+
) -> JacLogger:
|
|
150
|
+
"""
|
|
151
|
+
Get or create a configured logger instance.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
name : str
|
|
156
|
+
Logger name (default: "default")
|
|
157
|
+
console_log_level : int | None
|
|
158
|
+
Console handler level. Set to None to disable console logging.
|
|
159
|
+
log_dir : str | Path | LogDir | None
|
|
160
|
+
Directory for log files. LogDir.DEFAULT uses the system temp directory
|
|
161
|
+
under the sanitized name plus eight UUID4 hex characters. Explicit
|
|
162
|
+
directories use a unique filename. Set to None to disable file logging.
|
|
163
|
+
file_log_level : int | None
|
|
164
|
+
File handler level. Set to None to disable file logging.
|
|
165
|
+
use_colored_output : bool
|
|
166
|
+
Whether to use colored console output (default: True)
|
|
167
|
+
reinit_if_exists : bool
|
|
168
|
+
Delete an existing named logger and apply new settings (default: False).
|
|
169
|
+
Existing log files are preserved.
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
JacLogger
|
|
174
|
+
Configured package-owned logger, separate from logging.getLogger(name).
|
|
175
|
+
After fork, obtain a fresh logger in the child with get_logger().
|
|
176
|
+
"""
|
|
177
|
+
if not isinstance(name, str) or not name:
|
|
178
|
+
raise ValueError("Logger name must be a non-empty string.")
|
|
179
|
+
if reinit_if_exists:
|
|
180
|
+
delete_logger(name)
|
|
181
|
+
logger = _manager.getLogger(name)
|
|
182
|
+
|
|
183
|
+
# Avoid adding handlers multiple times
|
|
184
|
+
if logger.handlers:
|
|
185
|
+
warnings.warn(
|
|
186
|
+
f"Logger '{name}' already configured; returning cached logger "\
|
|
187
|
+
"and ignoring new settings.",
|
|
188
|
+
stacklevel=2
|
|
189
|
+
)
|
|
190
|
+
return logger
|
|
191
|
+
|
|
192
|
+
# Logger level must be INTERNAL to allow file handler to capture everything
|
|
193
|
+
# Individual handlers control their own output levels
|
|
194
|
+
logger.setLevel(INTERNAL_DEBUG)
|
|
195
|
+
logger.propagate = False
|
|
196
|
+
|
|
197
|
+
# Create file formatter
|
|
198
|
+
file_formatter = logging.Formatter(
|
|
199
|
+
"%(asctime)s.%(msecs)03d - %(name)s - %(levelname)s - %(message)s",
|
|
200
|
+
datefmt="%Y-%m-%d %H:%M:%S"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Console handler with coloredlogs
|
|
204
|
+
if console_log_level is not None:
|
|
205
|
+
if use_colored_output:
|
|
206
|
+
# coloredlogs installs directly on the logger with field-level colors
|
|
207
|
+
coloredlogs.install(
|
|
208
|
+
level=console_log_level,
|
|
209
|
+
logger=logger,
|
|
210
|
+
fmt="%(asctime)s.%(msecs)03d %(hostname)s %(name)s[%(process)d] "\
|
|
211
|
+
"%(levelname)s %(message)s",
|
|
212
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
213
|
+
level_styles={
|
|
214
|
+
"internal_debug": {"color": "blue"},
|
|
215
|
+
"internal_info": {"color": "white"},
|
|
216
|
+
"debug": {"color": "cyan"},
|
|
217
|
+
"info": {"color": "green"},
|
|
218
|
+
"warning": {"color": "yellow", "bold": True},
|
|
219
|
+
"error": {"color": "red", "bold": True},
|
|
220
|
+
"critical": {"color": "red", "bold": True, "background": "white"}
|
|
221
|
+
},
|
|
222
|
+
field_styles={
|
|
223
|
+
"asctime": {"color": "white", "bold": True},
|
|
224
|
+
"hostname": {"color": "magenta"},
|
|
225
|
+
"levelname": {"color": "white", "bold": True},
|
|
226
|
+
"name": {"color": "cyan"},
|
|
227
|
+
"programname": {"color": "cyan"},
|
|
228
|
+
"process": {"color": "magenta"}
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
console_handler = logging.StreamHandler()
|
|
233
|
+
console_handler.setLevel(console_log_level)
|
|
234
|
+
console_handler.setFormatter(file_formatter)
|
|
235
|
+
logger.addHandler(console_handler)
|
|
236
|
+
|
|
237
|
+
# File handler with rotation
|
|
238
|
+
if log_dir is not None and file_log_level is not None:
|
|
239
|
+
try:
|
|
240
|
+
log_filename = _create_log_path(name, log_dir)
|
|
241
|
+
file_handler = TimedRotatingFileHandler(
|
|
242
|
+
log_filename,
|
|
243
|
+
when="midnight",
|
|
244
|
+
interval=1,
|
|
245
|
+
backupCount=30,
|
|
246
|
+
encoding="utf-8"
|
|
247
|
+
)
|
|
248
|
+
file_handler.setLevel(file_log_level)
|
|
249
|
+
file_handler.setFormatter(file_formatter)
|
|
250
|
+
logger.addHandler(file_handler)
|
|
251
|
+
logger.info("Logging files to %s", log_filename.parent)
|
|
252
|
+
|
|
253
|
+
except (PermissionError, OSError) as e:
|
|
254
|
+
warnings.warn(
|
|
255
|
+
f"Could not create log file for '{name}' in {log_dir!r}: {e}. "
|
|
256
|
+
"File logging disabled.",
|
|
257
|
+
stacklevel=2
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return logger
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jacl4py
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Just another custom logger for Python.
|
|
5
|
+
Author-Email: Fabian Krumm <ghub_fk@gmx.de>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: coloredlogs>=15.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# jacl4py
|
|
15
|
+
|
|
16
|
+
Just another custom logger for Python.
|
|
17
|
+
|
|
18
|
+
`jacl4py` is a standalone package containing the custom Python logger previously
|
|
19
|
+
used by pookiepy.
|
|
20
|
+
|
|
21
|
+
## Release Status
|
|
22
|
+
|
|
23
|
+
Releases before `1.0.0` are testing versions and may introduce breaking changes
|
|
24
|
+
without a major version increment. Semantic versioning will begin with `1.0.0`.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Install from PyPI:
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
pip install jacl4py
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or install the local package with uv:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
uv pip install .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from jacl4py.logger import INTERNAL_INFO, get_logger
|
|
44
|
+
|
|
45
|
+
logger = get_logger("service")
|
|
46
|
+
logger.iinfo("framework lifecycle event")
|
|
47
|
+
logger.log(INTERNAL_INFO, "another internal event")
|
|
48
|
+
logger.info("service started")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Only `get_logger()` is needed to configure a logger. Package loggers use their
|
|
52
|
+
own registry, so importing the module does not replace Python's global logger
|
|
53
|
+
class. `logging.getLogger("service")` and `get_logger("service")` are separate
|
|
54
|
+
objects and do not change each other's handlers. The `INTERNAL_DEBUG` and
|
|
55
|
+
`INTERNAL_INFO` level names are registered with `logging`.
|
|
56
|
+
|
|
57
|
+
The console handler defaults to `INFO`; the file handler defaults to
|
|
58
|
+
`INTERNAL_DEBUG`. The default `log_dir=LogDir.DEFAULT` writes log files under the
|
|
59
|
+
system temporary directory in `<safe-name><uuid8>/<safe-name>.log`. For example,
|
|
60
|
+
`get_logger("test")` might write to `test12345678/test.log` under the temporary
|
|
61
|
+
directory. The suffix is the first eight hexadecimal characters of UUID4;
|
|
62
|
+
existing paths are never reused on a collision. Names are restricted to ASCII
|
|
63
|
+
letters, digits, dots, underscores, and hyphens for filesystem use, with other
|
|
64
|
+
characters replaced by underscores and the name component limited to 64
|
|
65
|
+
characters. The original logger name is preserved in log records.
|
|
66
|
+
|
|
67
|
+
Each writer has one stable active filename. Files rotate daily and retain up to
|
|
68
|
+
30 backups for that writer, not necessarily 30 calendar days. Reinitialization
|
|
69
|
+
and new processes receive fresh paths. Old run directories and file families
|
|
70
|
+
are preserved, not automatically pruned. The custom `iinfo()` and `idebug()`
|
|
71
|
+
methods emit at levels 7 and 5 respectively.
|
|
72
|
+
|
|
73
|
+
Pass `log_dir=None` to disable file logging, or a string or `Path` for a custom
|
|
74
|
+
directory. An explicit directory is used as given, with a unique filename
|
|
75
|
+
`<safe-name><uuid8>.log` inside it. Pass `None` for `console_log_level` or
|
|
76
|
+
`file_log_level` to disable that handler. Use an application-owned directory
|
|
77
|
+
when file permissions or durable retention matter.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from jacl4py.logger import LogDir, get_logger
|
|
81
|
+
|
|
82
|
+
logger = get_logger("service", log_dir=LogDir.DEFAULT)
|
|
83
|
+
console_only = get_logger("console", log_dir=None)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`JacLogger.setLevel()` follows standard logging behavior: it changes only the
|
|
87
|
+
logger threshold, preserving independent console and file handler levels.
|
|
88
|
+
Reusing a configured name returns the cached logger, emits a warning, and
|
|
89
|
+
ignores new configuration arguments. Initialization and deletion are serialized
|
|
90
|
+
across threads; ordinary log calls do not acquire the package lifecycle lock.
|
|
91
|
+
|
|
92
|
+
Pass `reinit_if_exists=True` to replace an existing logger and apply new settings
|
|
93
|
+
without the cached-logger warning:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from jacl4py.logger import delete_logger, get_logger
|
|
97
|
+
|
|
98
|
+
log = get_logger("my_test11", reinit_if_exists=True)
|
|
99
|
+
delete_logger("my_test11")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`delete_logger(name)` closes and removes the package logger's handlers and
|
|
103
|
+
unregisters it without touching standard loggers. It returns `True` if a package
|
|
104
|
+
logger was deleted, otherwise `False`. Existing log
|
|
105
|
+
files are preserved. Logging through a deleted `JacLogger` reference raises
|
|
106
|
+
`ResourceNotInitializedError`, a subclass of `RuntimeError`, even for messages
|
|
107
|
+
below the configured level. Import it from `jacl4py.logger` or
|
|
108
|
+
`jacl4py.exceptions`. Use the new object returned by `get_logger()` after
|
|
109
|
+
reinitialization. Package child loggers remain usable and reconnect when the
|
|
110
|
+
named logger is recreated. Use `get_logger()` for child names too.
|
|
111
|
+
|
|
112
|
+
In each worker process, call `get_logger()` to obtain a separate writer. After
|
|
113
|
+
`fork`, inherited package logger references are invalidated automatically; do
|
|
114
|
+
not continue logging through an inherited reference. No queue or listener is
|
|
115
|
+
required. Logging is synchronous, including disk writes and rollover, so it can
|
|
116
|
+
block application threads or an async event loop. Configure or replace loggers
|
|
117
|
+
before starting work rather than while other threads are emitting records.
|
|
118
|
+
|
|
119
|
+
The version is available from the imported logger module:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from jacl4py import logger
|
|
123
|
+
|
|
124
|
+
print(logger.__version__)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For a release, edit only `__version__` in `jacl4py/__init__.py`. PDM reads that
|
|
128
|
+
constant for wheel and source-distribution metadata; `jacl4py.__version__` and
|
|
129
|
+
`jacl4py.logger.__version__` expose the same value.
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
This standalone repository targets Python 3.10 or newer. It is authored by
|
|
134
|
+
Fabian Krumm and distributed under the BSD 3-Clause License from the original
|
|
135
|
+
project.
|
|
136
|
+
|
|
137
|
+
Run the unit tests with uv:
|
|
138
|
+
|
|
139
|
+
```powershell
|
|
140
|
+
uv pip install -r requirements.txt
|
|
141
|
+
uv run --with-requirements requirements.txt python -m unittest discover -s tests -p "test_*.py"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Install development tools, including Pylint and coverage:
|
|
145
|
+
|
|
146
|
+
```powershell
|
|
147
|
+
uv pip install -r requirements-dev.txt
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Run the same Pylint check as the push and pull request workflow:
|
|
151
|
+
|
|
152
|
+
```powershell
|
|
153
|
+
uv run --with-requirements requirements-dev.txt python -m pylint --jobs=1 jacl4py tests
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Check test coverage, including branches and missing lines:
|
|
157
|
+
|
|
158
|
+
```powershell
|
|
159
|
+
uv run --with-requirements requirements-dev.txt python -m coverage run --branch --source=jacl4py -m unittest discover -s tests -p "test_*.py"
|
|
160
|
+
uv run --with-requirements requirements-dev.txt python -m coverage report -m
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Build with:
|
|
164
|
+
|
|
165
|
+
```powershell
|
|
166
|
+
uv build
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`requirements.txt` contains runtime-only dependencies and mirrors
|
|
170
|
+
`pyproject.toml` project dependencies. The workflows use it before tests, build,
|
|
171
|
+
and publication preflight; the requirements guard test detects drift, so update
|
|
172
|
+
both files together. PDM's declared build backend supplies `uv build` build
|
|
173
|
+
dependencies, while the generated wheel and source-distribution metadata causes
|
|
174
|
+
pip installs to resolve runtime dependencies automatically. The requirements
|
|
175
|
+
files are included in source distributions. `requirements-dev.txt` includes
|
|
176
|
+
the runtime requirements and adds development-only tools. PDM includes the package source and
|
|
177
|
+
test/workflow sources directly, so a `MANIFEST.in` file is unnecessary.
|
|
178
|
+
|
|
179
|
+
## Publishing
|
|
180
|
+
|
|
181
|
+
GitHub Actions discovers workflows only from the repository root's
|
|
182
|
+
`.github/workflows` directory; workflows nested under `logger-package` are not
|
|
183
|
+
run automatically in this monorepo. When this package is published from its own
|
|
184
|
+
repository, keep these workflow files at that repository root.
|
|
185
|
+
|
|
186
|
+
`pypi_publish.yml` runs on pushes to `master`, not GitHub Release publication. A
|
|
187
|
+
read-only build job runs preflight unit tests, builds wheel and sdist, checks
|
|
188
|
+
them with Twine, smoke-tests each installed artifact, and uploads the validated
|
|
189
|
+
distributions. Only then does the `pypi-release` environment job publish those
|
|
190
|
+
artifacts through PyPI Trusted Publishing (OIDC, no stored API token), with
|
|
191
|
+
`skip-existing: true`, `print-hash: false`, and `attestations: true`.
|
|
192
|
+
|
|
193
|
+
After successful publication, it creates an annotated `v<version>` tag from
|
|
194
|
+
`jacl4py/__init__.py`. An existing tag on the same commit is an idempotent no-op;
|
|
195
|
+
a tag on another commit is never moved and causes failure. Edit the local
|
|
196
|
+
version constant manually before a new release: neither tags nor commit messages
|
|
197
|
+
infer or automatically bump the version. PDM reads that constant for metadata.
|
|
198
|
+
|
|
199
|
+
Configure the PyPI trusted publisher for project `jacl4py`, your GitHub
|
|
200
|
+
owner/repository, workflow filename `pypi_publish.yml`, and environment `pypi-release`.
|
|
201
|
+
Keep that filename to preserve the trusted publisher identity and retain the
|
|
202
|
+
environment URL `https://pypi.org/project/jacl4py/`. Confirm project availability
|
|
203
|
+
and choose an unpublished version before pushing. Skipping existing files allows
|
|
204
|
+
retries; it does not permit replacing a published version.
|
|
205
|
+
|
|
206
|
+
Publication depends on its own build job, not a mandatory CI `needs` barrier.
|
|
207
|
+
Protect `master` with required supported-version tests, lint, build and workflow
|
|
208
|
+
audit checks; protect the environment with reviewers and branch restrictions.
|
|
209
|
+
Floating `3.x` probes are nonblocking, not support guarantees. CI runs Python
|
|
210
|
+
3.10-3.14 on five runners, reports coverage without a minimum, and uses Pylint
|
|
211
|
+
3.2.6 with the default fail-under of 10 and no disabled messages or custom
|
|
212
|
+
threshold. See [.github/CI.md](.github/CI.md) for the matrix, weekly
|
|
213
|
+
checks, reference differences, and repository settings.
|
|
214
|
+
|
|
215
|
+
# todos
|
|
216
|
+
|
|
217
|
+
## ai findings
|
|
218
|
+
|
|
219
|
+
- [ ] Medium (#6): Benchmark synchronous logging under representative load.
|
|
220
|
+
Document an optional bounded queue/listener recipe only if needed, including
|
|
221
|
+
overflow behavior and flushing at shutdown. UUID paths isolate writers but do
|
|
222
|
+
not remove I/O latency.
|
|
223
|
+
- [ ] Medium (#7): Make intentionally disabled outputs silent with a
|
|
224
|
+
`NullHandler`; track configuration independently of handler presence so
|
|
225
|
+
Python's `lastResort` handler cannot unexpectedly emit to stderr.
|
|
226
|
+
- [ ] Medium (#8): Validate replacement settings and build handlers before
|
|
227
|
+
invalidating the working logger. Finish cleanup if one handler fails to close,
|
|
228
|
+
and consider an opt-in strict policy for required file logging.
|
|
229
|
+
- [ ] Medium (#9): Increment `stacklevel` in `iinfo()` and `idebug()` so caller
|
|
230
|
+
metadata identifies application code rather than the wrapper.
|
|
231
|
+
- [ ] Medium (#10): Extend regression coverage for output silence, caller
|
|
232
|
+
attribution, custom global logger factories, and file open/write/close
|
|
233
|
+
failures; add branch coverage reporting in CI. High-severity regression tests
|
|
234
|
+
have been added but still need execution.
|
|
235
|
+
- [ ] Medium (#11): Pin the PyPI publishing action to a reviewed commit; lock
|
|
236
|
+
release tooling, automate dependency updates and vulnerability audits, and
|
|
237
|
+
verify the release tag against built metadata. Keep consumer dependency ranges
|
|
238
|
+
separate from release-tool reproducibility.
|
|
239
|
+
- [ ] Low (#12): Add real project/support URLs, tested Python classifiers,
|
|
240
|
+
contribution and private security-reporting guidance, and a compatibility and
|
|
241
|
+
deprecation policy. Expand privacy, permissions, and application integration
|
|
242
|
+
guidance as the public API settles.
|
|
243
|
+
- [ ] Retention follow-up: Define opt-in cleanup of completed runs if total
|
|
244
|
+
disk usage must be bounded. Per-writer rotation does not delete older UUID
|
|
245
|
+
directories or file families; do not delete other writers' files implicitly.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
jacl4py-0.0.1.dist-info/METADATA,sha256=QMvS8Uexhh38Yz4CHeIDcPasmBRnDYDHPbDLobuIfww,10959
|
|
2
|
+
jacl4py-0.0.1.dist-info/WHEEL,sha256=IuIk3wbaEnFH1cx75iM2R1CmliRI3dFLXTbRakgvgFQ,90
|
|
3
|
+
jacl4py-0.0.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
jacl4py-0.0.1.dist-info/licenses/LICENSE,sha256=D5tHuFx3CsUmFQTursu3R7LmOAgCWqlytpquqGKWXdY,1499
|
|
5
|
+
jacl4py/__init__.py,sha256=aMADOnze8GsUON5ijtJD51SYZjnvFQQ6rz2uOOlp3M0,65
|
|
6
|
+
jacl4py/exceptions.py,sha256=9I1nP9oute88E6j0C6CTYhWd15urC6OAREv6tCRros8,172
|
|
7
|
+
jacl4py/logger.py,sha256=5mBE1Ve__XmhnmjloZDC5mWZ9SU7whCKwLL6nmRpF08,9339
|
|
8
|
+
jacl4py-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Fabian Krumm
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|