Coverage for sources/appcore/inscription.py: 100%

69 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-09-27 22:40 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Application inscription management. 

22 

23 Logging and, potentially, debug printing. 

24''' 

25# TODO? Add structured logging support (JSON formatting for log aggregation) 

26# TODO? Add distributed tracing support (correlation IDs, execution IDs) 

27# TODO? Add metrics collection and reporting 

28# TODO? Add OpenTelemetry integration 

29# TODO: Add TOML configuration support for inscription control settings 

30 

31 

32import logging as _logging 

33 

34from . import __ 

35from . import state as _state 

36 

37 

38Levels: __.typx.TypeAlias = __.typx.Literal[ 

39 'debug', 'info', 'warn', 'error', 'critical' ] 

40 

41 

42class Presentations( __.enum.Enum ): # TODO: Python 3.11: StrEnum 

43 ''' Scribe presentation modes. ''' 

44 

45 Null = 'null' # deferred to external management 

46 Plain = 'plain' # standard 

47 Rich = 'rich' # enhanced with Rich 

48 

49Modes = Presentations # deprecated 

50 

51 

52class TargetModes( __.enum.Enum ): # TODO: Python 3.11: StrEnum 

53 ''' Target file mode control. ''' 

54 

55 Append = 'append' 

56 Truncate = 'truncate' 

57 

58 

59class TargetDescriptor( __.immut.DataclassObject ): 

60 ''' Descriptor for file-based inscription targets. ''' 

61 

62 location: bytes | str | __.os.PathLike[ bytes ] | __.os.PathLike[ str ] 

63 mode: TargetModes = TargetModes.Truncate 

64 codec: str = 'utf-8' 

65 

66 

67Target: __.typx.TypeAlias = __.typx.Union[ 

68 __.io.TextIOWrapper, __.typx.TextIO, TargetDescriptor ] 

69 

70 

71class Control( __.immut.DataclassObject ): 

72 ''' Application inscription configuration. ''' 

73 

74 mode: Presentations = Presentations.Plain 

75 level: Levels = 'info' 

76 target: Target = __.sys.stderr 

77 

78 

79def prepare( auxdata: _state.Globals, /, control: Control ) -> None: 

80 ''' Prepares various scribes in a sensible manner. ''' 

81 target = _process_target( auxdata, control ) 

82 _prepare_scribes_logging( auxdata, control, target ) 

83 

84 

85def _discover_inscription_level_name( 

86 auxdata: _state.Globals, control: Control 

87) -> str: 

88 application_name = ''.join( 

89 c.upper( ) if c.isalnum( ) else '_' 

90 for c in auxdata.application.name ) 

91 for envvar_name_base in ( 'INSCRIPTION', 'LOG' ): 

92 envvar_name = ( 

93 "{name}_{base}_LEVEL".format( 

94 base = envvar_name_base, name = application_name ) ) 

95 if envvar_name in __.os.environ: 

96 return __.os.environ[ envvar_name ] 

97 return control.level 

98 

99 

100def _prepare_logging_plain( 

101 level: int, target: __.typx.TextIO, formatter: _logging.Formatter 

102) -> None: 

103 handler = _logging.StreamHandler( target ) 

104 handler.setFormatter( formatter ) 

105 _logging.basicConfig( 

106 force = True, level = level, handlers = ( handler, ) ) 

107 

108 

109def _prepare_logging_rich( 

110 level: int, target: __.typx.TextIO, formatter: _logging.Formatter 

111) -> None: 

112 try: 

113 from rich.console import Console 

114 from rich.logging import RichHandler 

115 except ImportError: 

116 # Gracefully degrade to plain mode. 

117 _prepare_logging_plain( level, target, formatter ) 

118 return 

119 console = Console( file = target ) 

120 handler = RichHandler( 

121 console = console, 

122 rich_tracebacks = True, 

123 show_path = False, show_time = True ) 

124 handler.setFormatter( formatter ) 

125 _logging.basicConfig( 

126 force = True, level = level, handlers = ( handler, ) ) 

127 

128 

129def _prepare_scribes_logging( 

130 auxdata: _state.Globals, control: Control, /, target: __.typx.TextIO 

131) -> None: 

132 level_name = _discover_inscription_level_name( auxdata, control ) 

133 level = getattr( _logging, level_name.upper( ) ) 

134 formatter = _logging.Formatter( "%(name)s: %(message)s" ) 

135 match control.mode: 

136 case Presentations.Plain: 

137 _prepare_logging_plain( level, target, formatter ) 

138 case Presentations.Rich: 

139 _prepare_logging_rich( level, target, formatter ) 

140 case _: pass 

141 

142 

143def _process_target( 

144 auxdata: _state.Globals, control: Control 

145) -> __.typx.TextIO: 

146 target = control.target 

147 if isinstance( target, __.typx.TextIO ): # pragma: no cover 

148 return target 

149 if isinstance( target, ( __.io.StringIO, __.io.TextIOWrapper ) ): 

150 return target 

151 location = target.location 

152 if isinstance( location, __.os.PathLike ): 

153 location = location.__fspath__( ) 

154 if isinstance( location, bytes ): 

155 location = location.decode( ) 

156 location = __.Path( location ) 

157 location.parent.mkdir( exist_ok = True, parents = True ) 

158 mode = 'w' if target.mode is TargetModes.Truncate else 'a' 

159 return auxdata.exits.enter_context( open( 

160 location, mode = mode, encoding = target.codec ) )