logger-36 2025.23__py3-none-any.whl → 2025.25__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.
Files changed (40) hide show
  1. logger_36/api/{gpu.py → chronos.py} +1 -1
  2. logger_36/api/memory.py +0 -9
  3. logger_36/api/storage.py +0 -1
  4. logger_36/catalog/handler/console.py +1 -1
  5. logger_36/catalog/handler/console_rich.py +33 -22
  6. logger_36/catalog/handler/file.py +1 -2
  7. logger_36/catalog/handler/generic.py +24 -10
  8. logger_36/catalog/handler/memory.py +31 -6
  9. logger_36/catalog/logger/chronos.py +10 -2
  10. logger_36/catalog/logger/memory.py +1 -1
  11. logger_36/catalog/logger/system.py +6 -5
  12. logger_36/config/message.py +1 -4
  13. logger_36/{api/system.py → config/rule.py} +2 -1
  14. logger_36/constant/chronos.py +50 -0
  15. logger_36/constant/error.py +2 -0
  16. logger_36/constant/html.py +2 -0
  17. logger_36/constant/message.py +7 -7
  18. logger_36/constant/path.py +8 -7
  19. logger_36/constant/record.py +2 -0
  20. logger_36/constant/rule.py +2 -2
  21. logger_36/{api/time.py → extension/file.py} +18 -2
  22. logger_36/{task → extension}/inspection.py +15 -29
  23. logger_36/extension/line.py +1 -1
  24. logger_36/task/format/memory.py +1 -1
  25. logger_36/task/format/message.py +7 -5
  26. logger_36/task/measure/chronos.py +21 -13
  27. logger_36/task/storage.py +32 -48
  28. logger_36/type/handler.py +26 -32
  29. logger_36/type/issue.py +7 -4
  30. logger_36/type/logger.py +164 -88
  31. logger_36/version.py +1 -1
  32. {logger_36-2025.23.dist-info → logger_36-2025.25.dist-info}/METADATA +30 -44
  33. logger_36-2025.25.dist-info/RECORD +53 -0
  34. logger_36/extension/html_.py +0 -93
  35. logger_36-2025.23.dist-info/RECORD +0 -53
  36. /logger_36/api/{type.py → logger.py} +0 -0
  37. /logger_36/api/{content.py → message.py} +0 -0
  38. /logger_36/{constant/generic.py → extension/sentinel.py} +0 -0
  39. {logger_36-2025.23.dist-info → logger_36-2025.25.dist-info}/WHEEL +0 -0
  40. {logger_36-2025.23.dist-info → logger_36-2025.25.dist-info}/top_level.txt +0 -0
@@ -7,8 +7,8 @@ SEE COPYRIGHT NOTICE BELOW
7
7
  import difflib as diff
8
8
  import typing as h
9
9
 
10
- from logger_36.constant.generic import NOT_PASSED
11
10
  from logger_36.constant.message import expected_op_h
11
+ from logger_36.extension.sentinel import NOT_PASSED
12
12
 
13
13
 
14
14
  def MessageWithActualExpected(
@@ -20,8 +20,10 @@ def MessageWithActualExpected(
20
20
  expected_is_choices: bool = False,
21
21
  expected_op: expected_op_h = "=",
22
22
  with_final_dot: bool = True,
23
- ) -> str:
24
- """"""
23
+ ) -> tuple[str, bool]:
24
+ """
25
+ Second return: has_actual_expected.
26
+ """
25
27
  if actual is NOT_PASSED:
26
28
  if with_final_dot:
27
29
  if message[-1] != ".":
@@ -29,7 +31,7 @@ def MessageWithActualExpected(
29
31
  elif message[-1] == ".":
30
32
  message = message[:-1]
31
33
 
32
- return message
34
+ return message, False
33
35
 
34
36
  if message[-1] == ".":
35
37
  message = message[:-1]
@@ -44,7 +46,7 @@ def MessageWithActualExpected(
44
46
  else:
45
47
  actual = f"{actual}:{type(actual).__name__}"
46
48
 
47
- return f"{message}: Actual={actual}; {expected}{dot}"
49
+ return f"{message}: Actual={actual}; {expected}{dot}", True
48
50
 
49
51
 
50
52
  def _FormattedExpected(
@@ -4,34 +4,42 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
4
4
  SEE COPYRIGHT NOTICE BELOW
5
5
  """
6
6
 
7
- import time
8
7
  from datetime import datetime as date_time_t
9
8
 
10
- # This module is imported early. Therefore, the current date and time should be close
11
- # enough to the real start time of the main script.
12
- _START_DATE_AND_TIME = date_time_t.now()
9
+ from logger_36.config.message import ELAPSED_TIME_SEPARATOR
10
+ from logger_36.constant.chronos import START_DATE_TIME
11
+ from logger_36.constant.message import TIME_LENGTH
13
12
 
14
13
 
15
14
  def TimeStamp(*, precision: str = "microseconds") -> str:
16
- """"""
15
+ """
16
+ precision: See documentation of date_time_t.isoformat.
17
+ """
17
18
  return (
18
19
  date_time_t.now()
19
20
  .isoformat(timespec=precision)
20
- .replace(".", "-")
21
+ .replace(".", "_")
21
22
  .replace(":", "-")
22
23
  )
23
24
 
24
25
 
25
- def ElapsedTime(*, should_return_now: bool = False) -> str | tuple[str, date_time_t]:
26
+ def FormattedElapsedTime(
27
+ now: date_time_t, /, *, reference: date_time_t = START_DATE_TIME
28
+ ) -> str:
26
29
  """"""
27
- now = date_time_t.now()
28
- elapsed_seconds = (now - _START_DATE_AND_TIME).total_seconds()
29
- output = time.strftime("%H:%M:%S", time.gmtime(elapsed_seconds))
30
+ output = str(now - reference)
31
+
32
+ if output.startswith("0:"):
33
+ output = output[2:]
30
34
  while output.startswith("00:"):
31
- output = output.split(sep=":", maxsplit=1)[-1]
35
+ output = output[3:]
36
+ if output[0] == "0":
37
+ output = output[1:]
38
+
39
+ output = ELAPSED_TIME_SEPARATOR + output
32
40
 
33
- if should_return_now:
34
- return output, now
41
+ if output.__len__() > TIME_LENGTH:
42
+ return output[:TIME_LENGTH]
35
43
  return output
36
44
 
37
45
 
logger_36/task/storage.py CHANGED
@@ -8,7 +8,14 @@ import logging as l
8
8
  from io import IOBase as io_base_t
9
9
  from pathlib import Path as path_t
10
10
 
11
- from logger_36.constant.html import BODY_PLACEHOLDER, MINIMAL_HTML, TITLE_PLACEHOLDER
11
+ from logger_36.constant.error import CANNOT_SAVE_RECORDS
12
+ from logger_36.constant.html import (
13
+ BODY_PLACEHOLDER,
14
+ HTML_SUFFIX,
15
+ MINIMAL_HTML,
16
+ TITLE_PLACEHOLDER,
17
+ )
18
+ from logger_36.extension.file import NewTemporaryFile
12
19
  from logger_36.instance.logger import L
13
20
  from logger_36.type.logger import logger_t
14
21
 
@@ -19,53 +26,17 @@ def SaveLOGasHTML(
19
26
  """
20
27
  From first console handler found.
21
28
  """
22
- cannot_save = "Cannot save logging record as HTML"
23
-
24
- if path is None:
25
- for handler in logger.handlers:
26
- if isinstance(handler, l.FileHandler):
27
- path = path_t(handler.baseFilename).with_suffix(".htm")
28
- break
29
- else:
30
- logger.warning(f"{cannot_save}: No file handler to build a filename from.")
31
- return
32
-
33
- if path.exists():
34
- logger.warning(
35
- f'{cannot_save}: Automatically generated path "{path}" already exists.'
36
- )
37
- return
38
- elif isinstance(path, str):
39
- path = path_t(path)
40
-
41
- actual_file = isinstance(path, path_t)
42
-
43
- if actual_file and path.exists():
44
- logger.warning(f'{cannot_save}: File "{path}" already exists.')
45
- return
46
-
47
- for handler in logger.handlers:
48
- records = getattr(handler, "records", None)
49
- if isinstance(records, list) and (
50
- (records.__len__() == 0)
51
- or all(
52
- isinstance(_, tuple)
53
- and isinstance(_[0], int)
54
- and isinstance(_[1], dict | str | l.LogRecord)
55
- and isinstance(_[2], bool)
56
- for _ in records
57
- )
58
- ):
59
- break
60
- else:
61
- logger.warning(f"{cannot_save}: No handlers with recording capability found.")
29
+ records = logger_t.Records(logger)
30
+ if records is None:
31
+ logger.warning(
32
+ f"{CANNOT_SAVE_RECORDS}: No handlers with recording capability found"
33
+ )
62
34
  return
63
-
64
35
  if records.__len__() == 0:
65
36
  return
66
37
 
67
38
  if isinstance(records[0][1], str):
68
- records = map(_HighlightedEvent, records)
39
+ records = map(_HighlightedRecord, records)
69
40
  else:
70
41
  records = map(lambda _: str(_[1]), records)
71
42
  body = "\n".join(records)
@@ -73,16 +44,29 @@ def SaveLOGasHTML(
73
44
  BODY_PLACEHOLDER, body
74
45
  )
75
46
 
76
- if actual_file:
77
- with open(path, "w") as accessor:
78
- accessor.write(html)
47
+ if path is None:
48
+ path = logger_t.StoragePath(logger, HTML_SUFFIX)
49
+ logger.info(f'Saving LOG as HTML in "{path}"')
50
+ elif isinstance(path, str):
51
+ path = path_t(path)
52
+ if path.exists():
53
+ existing = path
54
+ path = NewTemporaryFile(HTML_SUFFIX)
55
+ logger.warning(
56
+ f'File "{existing}" already exists: '
57
+ f'Saving LOG as HTML in "{path}" instead'
58
+ )
79
59
  else:
80
60
  path.write(html)
61
+ return
62
+
63
+ with open(path, "w") as accessor:
64
+ accessor.write(html)
81
65
 
82
66
 
83
- def _HighlightedEvent(event: tuple[int, str, bool], /) -> str:
67
+ def _HighlightedRecord(record: tuple[int, str, bool], /) -> str:
84
68
  """"""
85
- level, message, is_not_a_rule = event
69
+ level, message, is_not_a_rule = record
86
70
 
87
71
  if is_not_a_rule:
88
72
  if level == l.DEBUG:
logger_36/type/handler.py CHANGED
@@ -8,21 +8,16 @@ import logging as l
8
8
  import typing as h
9
9
  from pathlib import Path as path_t
10
10
 
11
- from logger_36.config.message import (
12
- FALLBACK_MESSAGE_WIDTH,
13
- LEVEL_CLOSING,
14
- LEVEL_OPENING,
15
- MESSAGE_MARKER,
16
- WHERE_SEPARATOR,
11
+ from logger_36.config.message import FALLBACK_MESSAGE_WIDTH
12
+ from logger_36.config.rule import DEFAULT_RULE_LENGTH, RULE_CHARACTER
13
+ from logger_36.constant.message import (
14
+ NEXT_LINE_PROLOGUE,
15
+ TIME_PLACEHOLDER,
16
+ WHERE_PROLOGUE,
17
+ CONTEXT_LENGTH_p_1,
17
18
  )
18
- from logger_36.constant.message import NEXT_LINE_PROLOGUE
19
19
  from logger_36.constant.record import SHOW_W_RULE_ATTR, WHEN_OR_ELAPSED_ATTR, WHERE_ATTR
20
- from logger_36.constant.rule import (
21
- DEFAULT_RULE,
22
- DEFAULT_RULE_LENGTH,
23
- MIN_HALF_RULE_LENGTH,
24
- RULE_CHARACTER,
25
- )
20
+ from logger_36.constant.rule import DEFAULT_RULE, MIN_HALF_RULE_LENGTH
26
21
  from logger_36.extension.line import WrappedLines
27
22
 
28
23
 
@@ -56,42 +51,41 @@ class extension_t:
56
51
  raise NotImplementedError
57
52
 
58
53
  def MessageFromRecord(
59
- self, record: l.LogRecord, /, *, rule_color: str | None = None
60
- ) -> tuple[str, bool]:
54
+ self, record: l.LogRecord, /, *, rule_color: str = "black"
55
+ ) -> tuple[str, bool, int | None]:
61
56
  """
62
- The second returned value is is_not_a_rule.
57
+ Arguments from second on: is_not_a_rule, where_location.
63
58
  """
64
59
  message = record.msg # See logger_36.catalog.handler.README.txt.
65
60
  if self.PreProcessedMessage is not None:
66
61
  message = self.PreProcessedMessage(message)
67
62
 
68
63
  if hasattr(record, SHOW_W_RULE_ATTR):
69
- return self.Rule(text=message, color=rule_color), False
64
+ return self.Rule(text=message, color=rule_color), False, None
70
65
 
71
66
  if (self.message_width <= 0) or (message.__len__() <= self.message_width):
72
67
  if "\n" in message:
73
68
  message = NEXT_LINE_PROLOGUE.join(message.splitlines())
74
69
  else:
75
70
  if "\n" in message:
76
- lines = WrappedLines(message.splitlines(), self.message_width)
71
+ lines = message.splitlines()
77
72
  else:
78
- lines = WrappedLines([message], self.message_width)
79
- message = NEXT_LINE_PROLOGUE.join(lines)
80
-
81
- when_or_elapsed = getattr(record, WHEN_OR_ELAPSED_ATTR, None)
82
- if when_or_elapsed is None:
83
- return message, True
73
+ lines = [message]
74
+ message = NEXT_LINE_PROLOGUE.join(WrappedLines(lines, self.message_width))
84
75
 
76
+ when_or_elapsed = getattr(record, WHEN_OR_ELAPSED_ATTR, TIME_PLACEHOLDER)
85
77
  if (where := getattr(record, WHERE_ATTR, None)) is None:
78
+ where_location = None
86
79
  where = ""
87
80
  else:
88
- where = f"{NEXT_LINE_PROLOGUE}{WHERE_SEPARATOR} {where}"
81
+ where_location = CONTEXT_LENGTH_p_1 + message.__len__()
82
+ where = f"{WHERE_PROLOGUE}{where}"
89
83
 
90
84
  return (
91
- f"{when_or_elapsed}"
92
- f"{LEVEL_OPENING}{record.levelname[0]}{LEVEL_CLOSING} "
93
- f"{MESSAGE_MARKER} {message}{where}"
94
- ), True
85
+ f"{when_or_elapsed}_{record.levelname[0].lower()} {message}{where}",
86
+ True,
87
+ where_location,
88
+ )
95
89
 
96
90
  def Rule(self, /, *, text: str | None = None, color: str = "black") -> str | h.Any:
97
91
  """
@@ -122,7 +116,7 @@ class extension_t:
122
116
  self.EmitMessage(self.Rule(text=text, color=color))
123
117
 
124
118
 
125
- class handler_t(l.Handler, extension_t):
119
+ class non_file_handler_t(l.Handler, extension_t):
126
120
  def __init__(
127
121
  self,
128
122
  name: str | None,
@@ -160,10 +154,10 @@ class file_handler_t(l.FileHandler, extension_t):
160
154
  __post_init__(self, level)
161
155
 
162
156
 
163
- any_handler_t = handler_t | file_handler_t
157
+ handler_h = non_file_handler_t | file_handler_t
164
158
 
165
159
 
166
- def __post_init__(handler: any_handler_t, level: int) -> None:
160
+ def __post_init__(handler: handler_h, level: int) -> None:
167
161
  """"""
168
162
  handler.setLevel(level)
169
163
 
logger_36/type/issue.py CHANGED
@@ -8,9 +8,9 @@ import logging as l
8
8
  import typing as h
9
9
 
10
10
  from logger_36.config.issue import ISSUE_BASE_CONTEXT
11
- from logger_36.constant.generic import NOT_PASSED
12
11
  from logger_36.constant.issue import ISSUE_LEVEL_SEPARATOR
13
12
  from logger_36.constant.message import expected_op_h
13
+ from logger_36.extension.sentinel import NOT_PASSED
14
14
  from logger_36.task.format.message import MessageWithActualExpected
15
15
 
16
16
  issue_t = str
@@ -28,11 +28,11 @@ def NewIssue(
28
28
  expected_is_choices: bool = False,
29
29
  expected_op: expected_op_h = "=",
30
30
  with_final_dot: bool = True,
31
- ) -> issue_t:
31
+ ) -> tuple[issue_t, bool]:
32
32
  """"""
33
33
  if context.__len__() == 0:
34
34
  context = ISSUE_BASE_CONTEXT
35
- message = MessageWithActualExpected(
35
+ message, has_actual_expected = MessageWithActualExpected(
36
36
  message,
37
37
  actual=actual,
38
38
  expected=expected,
@@ -41,7 +41,10 @@ def NewIssue(
41
41
  with_final_dot=with_final_dot,
42
42
  )
43
43
 
44
- return f"{level}{ISSUE_LEVEL_SEPARATOR}{context}{separator}{message}"
44
+ return (
45
+ f"{level}{ISSUE_LEVEL_SEPARATOR}{context}{separator}{message}",
46
+ has_actual_expected,
47
+ )
45
48
 
46
49
 
47
50
  """