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

72 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-19 00:58 +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 active_flavors: __.Absential[ __.ictr.ActiveFlavorsArgument ] = __.absent 

75 ictr_alias: __.Absential[ str ] = __.absent 

76 level: Levels = 'info' 

77 mode: Presentations = Presentations.Plain 

78 target: Target = __.sys.stderr 

79 trace_levels: __.Absential[ __.ictr.TraceLevelsArgument ] = __.absent 

80 

81 

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

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

84 target = _process_target( auxdata, control ) 

85 _prepare_scribes_logging( auxdata, control, target ) 

86 

87 

88def _discover_inscription_level_name( 

89 auxdata: _state.Globals, control: Control 

90) -> str: 

91 application_name = ''.join( 

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

93 for c in auxdata.application.name ) 

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

95 envvar_name = ( 

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

97 base = envvar_name_base, name = application_name ) ) 

98 if envvar_name in __.os.environ: 

99 return __.os.environ[ envvar_name ] 

100 return control.level 

101 

102 

103def _prepare_logging_plain( 

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

105) -> None: 

106 handler = _logging.StreamHandler( target ) 

107 handler.setFormatter( formatter ) 

108 _logging.basicConfig( 

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

110 

111 

112def _prepare_logging_rich( 

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

114) -> None: 

115 try: 

116 from rich.console import Console 

117 from rich.logging import RichHandler 

118 except ImportError: 

119 # Gracefully degrade to plain mode. 

120 _prepare_logging_plain( level, target, formatter ) 

121 return 

122 console = Console( file = target ) 

123 handler = RichHandler( 

124 console = console, 

125 rich_tracebacks = True, 

126 show_path = False, show_time = True ) 

127 handler.setFormatter( formatter ) 

128 _logging.basicConfig( 

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

130 

131 

132def _prepare_scribes_logging( 

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

134) -> None: 

135 level_name = _discover_inscription_level_name( auxdata, control ) 

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

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

138 match control.mode: 

139 case Presentations.Plain: 

140 _prepare_logging_plain( level, target, formatter ) 

141 case Presentations.Rich: 

142 _prepare_logging_rich( level, target, formatter ) 

143 case _: pass 

144 

145 

146def _process_target( 

147 auxdata: _state.Globals, control: Control 

148) -> __.typx.TextIO: 

149 target = control.target 

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

151 return target 

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

153 return target 

154 location = target.location 

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

156 location = location.__fspath__( ) 

157 if isinstance( location, bytes ): 

158 location = location.decode( ) 

159 location = __.Path( location ) 

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

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

162 return auxdata.exits.enter_context( open( 

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