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

68 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-19 22:17 +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 

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

39 ''' Scribe presentation modes. ''' 

40 

41 Null = 'null' # deferred to external management 

42 Plain = 'plain' # standard 

43 Rich = 'rich' # enhanced with Rich 

44 

45Modes = Presentations # deprecated 

46 

47 

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

49 ''' Target file mode control. ''' 

50 

51 Append = 'append' 

52 Truncate = 'truncate' 

53 

54 

55class TargetDescriptor( __.immut.DataclassObject ): 

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

57 

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

59 mode: TargetModes = TargetModes.Truncate 

60 codec: str = 'utf-8' 

61 

62 

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

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

65 

66 

67class Control( __.immut.DataclassObject ): 

68 ''' Application inscription configuration. ''' 

69 

70 mode: Presentations = Presentations.Plain 

71 level: __.typx.Literal[ 

72 'debug', 'info', 'warn', 'error', 'critical' # noqa: F821 

73 ] = 'info' 

74 target: Target = __.sys.stderr 

75 

76 

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

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

79 target = _process_target( auxdata, control ) 

80 _prepare_scribes_logging( auxdata, control, target ) 

81 

82 

83def _discover_inscription_level_name( 

84 auxdata: _state.Globals, control: Control 

85) -> str: 

86 application_name = ''.join( 

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

88 for c in auxdata.application.name ) 

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

90 envvar_name = ( 

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

92 base = envvar_name_base, name = application_name ) ) 

93 if envvar_name in __.os.environ: 

94 return __.os.environ[ envvar_name ] 

95 return control.level 

96 

97 

98def _prepare_logging_plain( 

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

100) -> None: 

101 handler = _logging.StreamHandler( target ) 

102 handler.setFormatter( formatter ) 

103 _logging.basicConfig( 

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

105 

106 

107def _prepare_logging_rich( 

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

109) -> None: 

110 try: 

111 from rich.console import Console 

112 from rich.logging import RichHandler 

113 except ImportError: 

114 # Gracefully degrade to plain mode. 

115 _prepare_logging_plain( level, target, formatter ) 

116 return 

117 console = Console( file = target ) 

118 handler = RichHandler( 

119 console = console, 

120 rich_tracebacks = True, 

121 show_path = False, show_time = True ) 

122 handler.setFormatter( formatter ) 

123 _logging.basicConfig( 

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

125 

126 

127def _prepare_scribes_logging( 

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

129) -> None: 

130 level_name = _discover_inscription_level_name( auxdata, control ) 

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

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

133 match control.mode: 

134 case Presentations.Plain: 

135 _prepare_logging_plain( level, target, formatter ) 

136 case Presentations.Rich: 

137 _prepare_logging_rich( level, target, formatter ) 

138 case _: pass 

139 

140 

141def _process_target( 

142 auxdata: _state.Globals, control: Control 

143) -> __.typx.TextIO: 

144 target = control.target 

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

146 return target 

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

148 return target 

149 location = target.location 

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

151 location = location.__fspath__( ) 

152 if isinstance( location, bytes ): 

153 location = location.decode( ) 

154 location = __.Path( location ) 

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

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

157 return auxdata.exits.enter_context( open( 

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