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
« 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 -*-
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#============================================================================#
21''' Application inscription management.
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
32import logging as _logging
34from . import __
35from . import state as _state
38Levels: __.typx.TypeAlias = __.typx.Literal[
39 'debug', 'info', 'warn', 'error', 'critical' ]
42class Presentations( __.enum.Enum ): # TODO: Python 3.11: StrEnum
43 ''' Scribe presentation modes. '''
45 Null = 'null' # deferred to external management
46 Plain = 'plain' # standard
47 Rich = 'rich' # enhanced with Rich
49Modes = Presentations # deprecated
52class TargetModes( __.enum.Enum ): # TODO: Python 3.11: StrEnum
53 ''' Target file mode control. '''
55 Append = 'append'
56 Truncate = 'truncate'
59class TargetDescriptor( __.immut.DataclassObject ):
60 ''' Descriptor for file-based inscription targets. '''
62 location: bytes | str | __.os.PathLike[ bytes ] | __.os.PathLike[ str ]
63 mode: TargetModes = TargetModes.Truncate
64 codec: str = 'utf-8'
67Target: __.typx.TypeAlias = __.typx.Union[
68 __.io.TextIOWrapper, __.typx.TextIO, TargetDescriptor ]
71class Control( __.immut.DataclassObject ):
72 ''' Application inscription configuration. '''
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
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 )
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
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, ) )
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, ) )
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
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 ) )