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
« 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 -*-
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 mode: Presentations = Presentations.Plain
75 level: Levels = 'info'
76 target: Target = __.sys.stderr
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 )
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
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, ) )
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, ) )
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
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 ) )