Coverage for sources/appcore/inscription.py: 100%
68 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-20 14:40 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-20 14:40 +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
38class Presentations( __.enum.Enum ): # TODO: Python 3.11: StrEnum
39 ''' Scribe presentation modes. '''
41 Null = 'null' # deferred to external management
42 Plain = 'plain' # standard
43 Rich = 'rich' # enhanced with Rich
45Modes = Presentations # deprecated
48class TargetModes( __.enum.Enum ): # TODO: Python 3.11: StrEnum
49 ''' Target file mode control. '''
51 Append = 'append'
52 Truncate = 'truncate'
55class TargetDescriptor( __.immut.DataclassObject ):
56 ''' Descriptor for file-based inscription targets. '''
58 location: bytes | str | __.os.PathLike[ bytes ] | __.os.PathLike[ str ]
59 mode: TargetModes = TargetModes.Truncate
60 codec: str = 'utf-8'
63Target: __.typx.TypeAlias = __.typx.Union[
64 __.io.TextIOWrapper, __.typx.TextIO, TargetDescriptor ]
67class Control( __.immut.DataclassObject ):
68 ''' Application inscription configuration. '''
70 mode: Presentations = Presentations.Plain
71 level: __.typx.Literal[
72 'debug', 'info', 'warn', 'error', 'critical' # noqa: F821
73 ] = 'info'
74 target: Target = __.sys.stderr
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 )
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
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, ) )
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, ) )
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
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 ) )