instruktai-python-logger 0.6.5__tar.gz → 0.6.7__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.
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/PKG-INFO +1 -1
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instrukt_ai_logging/__init__.py +17 -16
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instrukt_ai_logging/cli.py +24 -0
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instrukt_ai_logging/install.py +14 -11
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instrukt_ai_logging/logging.py +89 -1
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instruktai_python_logger.egg-info/PKG-INFO +1 -1
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/pyproject.toml +2 -2
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/tests/test_configure_logging.py +2 -3
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/tests/test_install.py +3 -9
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/tests/test_log_aggregation.py +2 -3
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/tests/test_trace_logging.py +2 -3
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/README.md +0 -0
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instrukt_ai_logging/py.typed +0 -0
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instruktai_python_logger.egg-info/SOURCES.txt +0 -0
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instruktai_python_logger.egg-info/dependency_links.txt +0 -0
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instruktai_python_logger.egg-info/entry_points.txt +0 -0
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instruktai_python_logger.egg-info/top_level.txt +0 -0
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/setup.cfg +0 -0
- {instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/tests/test_cli_follow.py +0 -0
{instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instrukt_ai_logging/__init__.py
RENAMED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
See `README.md` for usage and `docs/design.md` for design intent.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from importlib.metadata import PackageNotFoundError
|
|
7
|
+
from importlib.metadata import version as _pkg_version
|
|
8
|
+
|
|
9
|
+
from instrukt_ai_logging.logging import (
|
|
10
|
+
TRACE,
|
|
11
|
+
InstruktAILogger,
|
|
12
|
+
InstruktAILoggerProtocol,
|
|
13
|
+
configure_logging,
|
|
14
|
+
get_logger,
|
|
15
|
+
resolve_log_file,
|
|
16
|
+
resolve_log_files,
|
|
17
|
+
)
|
|
18
|
+
|
|
6
19
|
__all__ = [
|
|
7
20
|
"TRACE",
|
|
8
21
|
"InstruktAILogger",
|
|
@@ -14,21 +27,9 @@ __all__ = [
|
|
|
14
27
|
"resolve_log_files",
|
|
15
28
|
]
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
from importlib.metadata import packages_distributions
|
|
19
|
-
from importlib.metadata import version as _pkg_version
|
|
30
|
+
_DISTRIBUTION_NAME = "instruktai-python-logger"
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
__version__ = _pkg_version(
|
|
23
|
-
except
|
|
32
|
+
try:
|
|
33
|
+
__version__ = _pkg_version(_DISTRIBUTION_NAME)
|
|
34
|
+
except PackageNotFoundError:
|
|
24
35
|
__version__ = "0.0.0"
|
|
25
|
-
|
|
26
|
-
from instrukt_ai_logging.logging import (
|
|
27
|
-
TRACE,
|
|
28
|
-
InstruktAILogger,
|
|
29
|
-
InstruktAILoggerProtocol,
|
|
30
|
-
configure_logging,
|
|
31
|
-
get_logger,
|
|
32
|
-
resolve_log_file,
|
|
33
|
-
resolve_log_files,
|
|
34
|
-
)
|
{instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instrukt_ai_logging/cli.py
RENAMED
|
@@ -7,10 +7,12 @@ import sys
|
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
9
|
from collections.abc import Iterator
|
|
10
|
+
from datetime import datetime
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
12
13
|
from instrukt_ai_logging.logging import (
|
|
13
14
|
iter_recent_log_lines_merged,
|
|
15
|
+
newest_log_timestamp,
|
|
14
16
|
parse_since,
|
|
15
17
|
resolve_log_files,
|
|
16
18
|
)
|
|
@@ -98,6 +100,18 @@ def iter_follow_lines(
|
|
|
98
100
|
pass
|
|
99
101
|
|
|
100
102
|
|
|
103
|
+
_TIMESTAMP_STRFTIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
104
|
+
_TIMESTAMP_MS_SUFFIX_TEMPLATE = ".{ms:03d}Z"
|
|
105
|
+
_NO_TIMESTAMPED_ENTRY_LABEL = "no timestamped entry"
|
|
106
|
+
_EMPTY_RESULT_NOTE_TEMPLATE = "No lines matched. Resolved log root: {log_root}. Newest entry in that root: {newest}."
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _format_timestamp(ts: datetime) -> str:
|
|
110
|
+
"""Format a timestamp the same way log lines carry theirs (ms precision, `Z`)."""
|
|
111
|
+
ms = int(ts.microsecond / 1000)
|
|
112
|
+
return ts.strftime(_TIMESTAMP_STRFTIME_FORMAT) + _TIMESTAMP_MS_SUFFIX_TEMPLATE.format(ms=ms)
|
|
113
|
+
|
|
114
|
+
|
|
101
115
|
def _parse_stems(value: str) -> list[str]:
|
|
102
116
|
return [item.strip() for item in value.split(",") if item.strip()]
|
|
103
117
|
|
|
@@ -236,11 +250,21 @@ def main() -> None:
|
|
|
236
250
|
raise SystemExit(f"No log files matched stems {stems} in app '{args.app}'. Available: {available_str}")
|
|
237
251
|
raise SystemExit(f"No log files found for app '{args.app}'")
|
|
238
252
|
|
|
253
|
+
wrote_a_line = False
|
|
239
254
|
for line in iter_recent_log_lines_merged(files, since):
|
|
240
255
|
if _line_passes(line, keep=pattern, drop=exclude_pattern):
|
|
241
256
|
sys.stdout.write(line)
|
|
257
|
+
wrote_a_line = True
|
|
242
258
|
|
|
243
259
|
if not args.follow:
|
|
260
|
+
if not wrote_a_line:
|
|
261
|
+
log_root = files[0].parent
|
|
262
|
+
newest = newest_log_timestamp(files)
|
|
263
|
+
newest_desc = _format_timestamp(newest) if newest is not None else _NO_TIMESTAMPED_ENTRY_LABEL
|
|
264
|
+
print(
|
|
265
|
+
_EMPTY_RESULT_NOTE_TEMPLATE.format(log_root=log_root, newest=newest_desc),
|
|
266
|
+
file=sys.stderr,
|
|
267
|
+
)
|
|
244
268
|
return
|
|
245
269
|
|
|
246
270
|
sys.stdout.flush()
|
{instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instrukt_ai_logging/install.py
RENAMED
|
@@ -17,6 +17,17 @@ from pathlib import Path
|
|
|
17
17
|
_NEWSYSLOG_BIN = "/usr/sbin/newsyslog"
|
|
18
18
|
_LAUNCHD_LABEL = "ai.instrukt.log-rotate"
|
|
19
19
|
_SYSTEMD_UNIT_NAME = "instrukt-ai-logrotate"
|
|
20
|
+
_ARCHIVE_DEPTH = 30
|
|
21
|
+
_NEWSYSLOG_CONF_TEMPLATE = "{log_root}/*/*.log 640 {archive_depth} 50000 * ZG\n"
|
|
22
|
+
_LOGROTATE_CONF_TEMPLATE = """{log_root}/*/*.log {{
|
|
23
|
+
size 50M
|
|
24
|
+
rotate {archive_depth}
|
|
25
|
+
compress
|
|
26
|
+
delaycompress
|
|
27
|
+
missingok
|
|
28
|
+
notifempty
|
|
29
|
+
}}
|
|
30
|
+
"""
|
|
20
31
|
|
|
21
32
|
|
|
22
33
|
def _state_home_root() -> Path:
|
|
@@ -55,8 +66,8 @@ def _systemd_timer_path() -> Path:
|
|
|
55
66
|
|
|
56
67
|
|
|
57
68
|
def _newsyslog_conf(log_root: Path) -> str:
|
|
58
|
-
"""One glob line
|
|
59
|
-
return
|
|
69
|
+
"""One glob line for gzip-compressed archive retention."""
|
|
70
|
+
return _NEWSYSLOG_CONF_TEMPLATE.format(log_root=log_root, archive_depth=_ARCHIVE_DEPTH)
|
|
60
71
|
|
|
61
72
|
|
|
62
73
|
def _launchd_plist(conf_path: Path) -> str:
|
|
@@ -87,15 +98,7 @@ def _launchd_plist(conf_path: Path) -> str:
|
|
|
87
98
|
|
|
88
99
|
def _logrotate_conf(log_root: Path) -> str:
|
|
89
100
|
"""Default move+create rotation; WatchedFileHandler reopens on inode change."""
|
|
90
|
-
return
|
|
91
|
-
size 50M
|
|
92
|
-
rotate 5
|
|
93
|
-
compress
|
|
94
|
-
delaycompress
|
|
95
|
-
missingok
|
|
96
|
-
notifempty
|
|
97
|
-
}}
|
|
98
|
-
"""
|
|
101
|
+
return _LOGROTATE_CONF_TEMPLATE.format(log_root=log_root, archive_depth=_ARCHIVE_DEPTH)
|
|
99
102
|
|
|
100
103
|
|
|
101
104
|
def _systemd_service(conf_path: Path, state_file: Path) -> str:
|
{instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instrukt_ai_logging/logging.py
RENAMED
|
@@ -10,7 +10,7 @@ from dataclasses import dataclass
|
|
|
10
10
|
from datetime import UTC, datetime, timedelta
|
|
11
11
|
from logging.handlers import WatchedFileHandler
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any, Protocol, cast, runtime_checkable
|
|
13
|
+
from typing import Any, Literal, Protocol, cast, runtime_checkable
|
|
14
14
|
|
|
15
15
|
# Standard logging levels are: NOTSET=0, DEBUG=10, INFO=20, WARNING=30, ERROR=40, CRITICAL=50
|
|
16
16
|
TRACE: int = 5
|
|
@@ -587,6 +587,94 @@ def _window_start_offset(path: Path, cutoff: datetime) -> int:
|
|
|
587
587
|
return 0
|
|
588
588
|
|
|
589
589
|
|
|
590
|
+
_GZIP_SUFFIX = ".gz"
|
|
591
|
+
_BINARY_READ_MODE: Literal["rb"] = "rb"
|
|
592
|
+
_TEXT_DECODE_ENCODING = "utf-8"
|
|
593
|
+
_TEXT_DECODE_ERRORS = "replace"
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _tail_timestamp(path: Path) -> datetime | None:
|
|
597
|
+
"""Return the newest parsable log timestamp near the tail of `path`, or `None`.
|
|
598
|
+
|
|
599
|
+
A log file is appended in timestamp order, so its newest entry is always at
|
|
600
|
+
the tail: probe backward in exponentially growing chunks (the same
|
|
601
|
+
`_WINDOW_PROBE_BYTES` doubling idiom `_window_start_offset` uses) until the
|
|
602
|
+
probed span holds at least one parsable timestamp, then return the last one
|
|
603
|
+
seen while scanning that span forward to EOF — ascending order makes that the
|
|
604
|
+
maximum. Falls back to a full scan only when no probe finds a timestamp at
|
|
605
|
+
all. Gzip archives have no seek and are streamed from the start; the caller's
|
|
606
|
+
mtime bound already governs whether this is called on one.
|
|
607
|
+
|
|
608
|
+
Only a vanished file (`FileNotFoundError`, the same race every sibling probe
|
|
609
|
+
in this module already tolerates) is treated as absence. A file that exists
|
|
610
|
+
but cannot be read or decompressed — permission denied, a truncated or
|
|
611
|
+
corrupt archive — propagates its exception rather than being reported as
|
|
612
|
+
holding no timestamp.
|
|
613
|
+
"""
|
|
614
|
+
if path.name.endswith(_GZIP_SUFFIX):
|
|
615
|
+
newest: datetime | None = None
|
|
616
|
+
try:
|
|
617
|
+
with gzip.open(path, _BINARY_READ_MODE) as fb:
|
|
618
|
+
for raw in fb:
|
|
619
|
+
ts = parse_log_timestamp(raw.decode(_TEXT_DECODE_ENCODING, errors=_TEXT_DECODE_ERRORS))
|
|
620
|
+
if ts is not None:
|
|
621
|
+
newest = ts
|
|
622
|
+
except FileNotFoundError:
|
|
623
|
+
return None
|
|
624
|
+
return newest
|
|
625
|
+
|
|
626
|
+
try:
|
|
627
|
+
size = path.stat().st_size
|
|
628
|
+
except FileNotFoundError:
|
|
629
|
+
return None
|
|
630
|
+
|
|
631
|
+
probe = _WINDOW_PROBE_BYTES
|
|
632
|
+
with path.open(_BINARY_READ_MODE) as fb:
|
|
633
|
+
while True:
|
|
634
|
+
start = max(0, size - probe)
|
|
635
|
+
fb.seek(start)
|
|
636
|
+
if start:
|
|
637
|
+
fb.readline() # discard the partial line spanning the probe boundary
|
|
638
|
+
newest = None
|
|
639
|
+
for raw in fb:
|
|
640
|
+
ts = parse_log_timestamp(raw.decode(_TEXT_DECODE_ENCODING, errors=_TEXT_DECODE_ERRORS))
|
|
641
|
+
if ts is not None:
|
|
642
|
+
newest = ts
|
|
643
|
+
if newest is not None or start == 0:
|
|
644
|
+
return newest
|
|
645
|
+
probe *= 2
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def newest_log_timestamp(files: Iterable[Path]) -> datetime | None:
|
|
649
|
+
"""Return the newest parsable log timestamp across `files`, or `None`.
|
|
650
|
+
|
|
651
|
+
Examines files in descending mtime order. A file's entries never postdate
|
|
652
|
+
its own mtime — the bound `iter_recent_log_lines_merged` already relies on
|
|
653
|
+
to skip whole files — so once a remaining candidate's mtime can no longer
|
|
654
|
+
beat the best timestamp found so far, no later candidate can either and the
|
|
655
|
+
scan stops. A single file's mtime is not enough on its own to answer this:
|
|
656
|
+
the root holds independent per-stem streams and their rotations, so the
|
|
657
|
+
newest entry can live in a file that was not the most recently touched.
|
|
658
|
+
"""
|
|
659
|
+
candidates: list[tuple[float, Path]] = []
|
|
660
|
+
for path in files:
|
|
661
|
+
try:
|
|
662
|
+
mtime = path.stat().st_mtime
|
|
663
|
+
except FileNotFoundError:
|
|
664
|
+
continue
|
|
665
|
+
candidates.append((mtime, path))
|
|
666
|
+
candidates.sort(key=lambda item: item[0], reverse=True)
|
|
667
|
+
|
|
668
|
+
best: datetime | None = None
|
|
669
|
+
for mtime, path in candidates:
|
|
670
|
+
if best is not None and datetime.fromtimestamp(mtime, tz=UTC) <= best:
|
|
671
|
+
break
|
|
672
|
+
ts = _tail_timestamp(path)
|
|
673
|
+
if ts is not None and (best is None or ts > best):
|
|
674
|
+
best = ts
|
|
675
|
+
return best
|
|
676
|
+
|
|
677
|
+
|
|
590
678
|
def iter_recent_log_lines_merged(files: Iterable[Path], since: timedelta) -> Iterator[str]:
|
|
591
679
|
"""Yield lines newer than now-`since` merged across multiple files by timestamp.
|
|
592
680
|
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "instruktai-python-logger"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.7"
|
|
8
8
|
description = "Centralized logging utilities for Python services."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
dependencies = []
|
|
12
12
|
|
|
13
13
|
[dependency-groups]
|
|
14
|
-
dev = ["pytest>=8", "pytest-timeout>=2.2.0", "ruff>=0.8", "pyright>=1.1", "mypy>=1.10"]
|
|
14
|
+
dev = ["pytest>=8", "pytest-timeout>=2.2.0", "ruff>=0.8", "pyright>=1.1", "mypy>=1.10", "syrupy>=4"]
|
|
15
15
|
|
|
16
16
|
[project.scripts]
|
|
17
17
|
instrukt-ai-logs = "instrukt_ai_logging.cli:main"
|
{instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/tests/test_configure_logging.py
RENAMED
|
@@ -7,9 +7,8 @@ from tempfile import TemporaryDirectory
|
|
|
7
7
|
import pytest
|
|
8
8
|
from instrukt_ai_logging import InstruktAILogger, configure_logging, get_logger
|
|
9
9
|
|
|
10
|
-
# Log markers shared by each test and its assertions — named
|
|
11
|
-
#
|
|
12
|
-
# assertions). The logfmt field fragments are derived from the markers so the
|
|
10
|
+
# Log markers shared by each test and its assertions — named
|
|
11
|
+
# constants. The logfmt field fragments are derived from the markers so the
|
|
13
12
|
# expected output stays in sync with what is logged.
|
|
14
13
|
_LOGGER_OURS = "teleclaude.core"
|
|
15
14
|
_LOGGER_HTTPCORE = "httpcore.http11"
|
|
@@ -8,12 +8,9 @@ from pathlib import Path
|
|
|
8
8
|
import pytest
|
|
9
9
|
from instrukt_ai_logging import install
|
|
10
10
|
|
|
11
|
-
# Protocol-significant tokens
|
|
12
|
-
#
|
|
13
|
-
# assertions). These are newsyslog/logrotate/launchd/systemd field identities,
|
|
14
|
-
# not prose.
|
|
11
|
+
# Protocol-significant tokens the rotation-config assertions check for, named as
|
|
12
|
+
# constants. These are newsyslog/logrotate/launchd/systemd field identities, not prose.
|
|
15
13
|
_NEWSYSLOG_MODE = "640"
|
|
16
|
-
_NEWSYSLOG_COUNT = "5"
|
|
17
14
|
_NEWSYSLOG_SIZE_KB = "50000"
|
|
18
15
|
_NEWSYSLOG_WHEN = "*"
|
|
19
16
|
_NEWSYSLOG_COMPRESS_GLOB_FLAGS = "ZG" # gzip-compress archives; glob-expand at rotation time
|
|
@@ -22,7 +19,6 @@ _PLIST_PROGRAM = "/usr/sbin/newsyslog"
|
|
|
22
19
|
_PLIST_NO_ROOT_FLAG = "<string>-r</string>" # newsyslog otherwise refuses to run as a non-root user
|
|
23
20
|
_PLIST_START_INTERVAL = "1800"
|
|
24
21
|
_LOGROTATE_SIZE = "size 50M"
|
|
25
|
-
_LOGROTATE_ROTATE = "rotate 5"
|
|
26
22
|
_LOGROTATE_COMPRESS = "compress"
|
|
27
23
|
_LOGROTATE_DELAYCOMPRESS = "delaycompress"
|
|
28
24
|
_LOGROTATE_MISSINGOK = "missingok"
|
|
@@ -82,10 +78,9 @@ def test_ensure_rotation_darwin_writes_newsyslog_conf_and_plist(
|
|
|
82
78
|
|
|
83
79
|
assert problems == []
|
|
84
80
|
conf_content = install._newsyslog_conf_path().read_text(encoding="utf-8")
|
|
85
|
-
path, mode,
|
|
81
|
+
path, mode, _count, size_kb, when, flags = conf_content.split()
|
|
86
82
|
assert path == f"{rotation_env / 'instrukt-ai'}/*/*.log"
|
|
87
83
|
assert mode == _NEWSYSLOG_MODE
|
|
88
|
-
assert count == _NEWSYSLOG_COUNT
|
|
89
84
|
assert size_kb == _NEWSYSLOG_SIZE_KB
|
|
90
85
|
assert when == _NEWSYSLOG_WHEN
|
|
91
86
|
assert flags == _NEWSYSLOG_COMPRESS_GLOB_FLAGS
|
|
@@ -115,7 +110,6 @@ def test_ensure_rotation_linux_writes_logrotate_conf_and_systemd_units(
|
|
|
115
110
|
assert f"{rotation_env / 'instrukt-ai'}/*/*.log" in logrotate_content
|
|
116
111
|
for directive in (
|
|
117
112
|
_LOGROTATE_SIZE,
|
|
118
|
-
_LOGROTATE_ROTATE,
|
|
119
113
|
_LOGROTATE_COMPRESS,
|
|
120
114
|
_LOGROTATE_DELAYCOMPRESS,
|
|
121
115
|
_LOGROTATE_MISSINGOK,
|
{instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/tests/test_log_aggregation.py
RENAMED
|
@@ -19,9 +19,8 @@ def _write(path: Path, content: str) -> None:
|
|
|
19
19
|
path.write_text(content, encoding="utf-8")
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
# Log markers shared by each fixture and its assertions — named
|
|
23
|
-
#
|
|
24
|
-
# assertions).
|
|
22
|
+
# Log markers shared by each fixture and its assertions — named
|
|
23
|
+
# constants.
|
|
25
24
|
_MSG_FRESH = "fresh"
|
|
26
25
|
_MSG_BOOM = "boom"
|
|
27
26
|
_MSG_AFTER = "after"
|
{instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/tests/test_trace_logging.py
RENAMED
|
@@ -7,9 +7,8 @@ from instrukt_ai_logging.logging import (
|
|
|
7
7
|
get_logger,
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
-
# Log markers shared by each test and its assertions — named
|
|
11
|
-
#
|
|
12
|
-
# assertions). The logfmt field fragments are derived from the markers so the
|
|
10
|
+
# Log markers shared by each test and its assertions — named
|
|
11
|
+
# constants. The logfmt field fragments are derived from the markers so the
|
|
13
12
|
# expected output stays in sync with what is logged.
|
|
14
13
|
_TRACE_MSG = "This is a trace message"
|
|
15
14
|
_DEBUG_MSG = "This is a debug message"
|
|
File without changes
|
{instruktai_python_logger-0.6.5 → instruktai_python_logger-0.6.7}/instrukt_ai_logging/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|