Configuration Management¶
Introduction¶
The appcore
package provides robust TOML-based configuration management
with support for hierarchical includes, template variables, and runtime edits.
Configuration files are automatically discovered and merged according to
precedence rules.
Basic TOML Configuration¶
Configuration files use TOML format and are automatically loaded during application initialization:
import asyncio
import contextlib
import appcore
async def main( ):
async with contextlib.AsyncExitStack( ) as exits:
globals_dto = await appcore.prepare( exits )
# Access loaded configuration
config = globals_dto.configuration
print( f"Configuration keys: {list( config.keys( ) )}" )
# Configuration is an accretive dictionary
print( f"Type: {type( config )}" )
if __name__ == '__main__':
asyncio.run( main( ) )
The configuration system looks for a general.toml
file in your user
configuration directory. If it doesn’t exist, a template is copied from
the package data.
Configuration File Locations¶
Configuration files are searched in platform-specific locations:
import asyncio
import contextlib
import appcore
async def main( ):
async with contextlib.AsyncExitStack( ) as exits:
globals_dto = await appcore.prepare( exits )
# Show where configuration files are expected
config_dir = globals_dto.directories.user_config_path
print( f"Configuration directory: {config_dir}" )
print( f"Default config file: {config_dir / 'general.toml'}" )
if __name__ == '__main__':
asyncio.run( main( ) )
Platform-specific locations:
Linux:
~/.config/appname/general.toml
macOS:
~/Library/Application Support/appname/general.toml
Windows:
%APPDATA%\\appname\\general.toml
Stream-Based Configuration¶
For testing or dynamic configuration, you can provide configuration content directly as a stream:
>>> import io
>>> import appcore
>>>
>>> # Create configuration content
>>> config_content = '''
... [application]
... debug = true
... timeout = 30
...
... [logging]
... level = "debug"
... '''
>>>
>>> # Use StringIO for stream-based configuration
>>> config_stream = io.StringIO( config_content )
import asyncio
import contextlib
import io
import appcore
async def main( ):
# Create configuration content
config_content = '''
[application]
debug = true
timeout = 30
[logging]
level = "debug"
'''
config_stream = io.StringIO( config_content )
async with contextlib.AsyncExitStack( ) as exits:
globals_dto = await appcore.prepare(
exits,
configfile = config_stream
)
config = globals_dto.configuration
print( f"Debug mode: {config[ 'application' ][ 'debug' ]}" )
print( f"Timeout: {config[ 'application' ][ 'timeout' ]}" )
if __name__ == '__main__':
asyncio.run( main( ) )
Hierarchical Configuration with Includes¶
Configuration files can include other files using the includes
directive:
# main configuration file
[application]
name = "myapp"
[includes]
# Include files from user configuration directory
specs = [
"{user_configuration}/local.toml",
"{user_configuration}/overrides/"
]
import asyncio
import contextlib
import appcore
async def main( ):
async with contextlib.AsyncExitStack( ) as exits:
globals_dto = await appcore.prepare( exits )
config = globals_dto.configuration
# Configuration from includes is merged
print( f"Final configuration: {dict( config )}" )
# Later includes override earlier ones
# Files in directories are loaded alphabetically
if __name__ == '__main__':
asyncio.run( main( ) )
Include variables available:
{user_configuration}
- User configuration directory{user_home}
- User home directory{application_name}
- Application name
Configuration Templates and Variables¶
Configuration paths support template variables for dynamic resolution:
[locations]
data = "{user_home}/Documents/{application_name}"
cache = "{user_cache}/custom-cache"
[includes]
specs = [
"{user_configuration}/{application_name}-local.toml"
]
import asyncio
import contextlib
import appcore
async def main( ):
# Custom application name affects template resolution
app_info = appcore.ApplicationInformation( name = 'my-custom-app' )
async with contextlib.AsyncExitStack( ) as exits:
globals_dto = await appcore.prepare(
exits,
application = app_info
)
# Template variables are resolved automatically
data_location = globals_dto.provide_data_location( )
print( f"Data location: {data_location}" )
# Will use custom paths if configured
if __name__ == '__main__':
asyncio.run( main( ) )
Runtime Configuration Edits¶
You can modify configuration at runtime using edit functions:
>>> import appcore
>>>
>>> def enable_debug_mode( config ):
... ''' Enable debug mode in configuration. '''
... if 'application' not in config:
... config[ 'application' ] = { }
... config[ 'application' ][ 'debug' ] = True
...
>>> def set_log_level( config ):
... ''' Set logging level to debug. '''
... if 'logging' not in config:
... config[ 'logging' ] = { }
... config[ 'logging' ][ 'level' ] = 'debug'
import asyncio
import contextlib
import appcore
def enable_debug_mode( config ):
''' Enable debug mode in configuration. '''
if 'application' not in config:
config[ 'application' ] = { }
config[ 'application' ][ 'debug' ] = True
def set_log_level( config ):
''' Set logging level to debug. '''
if 'logging' not in config:
config[ 'logging' ] = { }
config[ 'logging' ][ 'level' ] = 'debug'
async def main( ):
# Apply configuration edits during initialization
edits = ( enable_debug_mode, set_log_level )
async with contextlib.AsyncExitStack( ) as exits:
globals_dto = await appcore.prepare(
exits,
configedits = edits
)
config = globals_dto.configuration
print( f"Debug enabled: {config.get( 'application', { } ).get( 'debug' )}" )
print( f"Log level: {config.get( 'logging', { } ).get( 'level' )}" )
if __name__ == '__main__':
asyncio.run( main( ) )
Error Handling¶
Configuration loading can encounter various error conditions:
import asyncio
import contextlib
import io
import appcore
async def main( ):
# Invalid TOML content
invalid_toml = '''
[application
debug = true # missing closing bracket
'''
config_stream = io.StringIO( invalid_toml )
try:
async with contextlib.AsyncExitStack( ) as exits:
globals_dto = await appcore.prepare(
exits,
configfile = config_stream
)
except Exception as e:
print( f"Configuration error: {e}" )
# Handle TOML parsing errors gracefully
print( 'Using default configuration instead' )
if __name__ == '__main__':
asyncio.run( main( ) )
Custom Configuration Acquirers¶
You can customize configuration loading by providing custom acquirers:
>>> import appcore
>>>
>>> # Custom acquirer with different template filename
>>> acquirer = appcore.TomlConfigurationAcquirer( main_filename = 'myapp.toml' )
>>> type( acquirer )
<class 'appcore.configuration.TomlAcquirer'>
>>> acquirer.main_filename
'myapp.toml'
>>> acquirer.includes_name
'includes'
import asyncio
import contextlib
import appcore
async def main( ):
# Use custom template filename
acquirer = appcore.TomlConfigurationAcquirer( main_filename = 'myapp.toml' )
async with contextlib.AsyncExitStack( ) as exits:
globals_dto = await appcore.prepare(
exits,
acquirer = acquirer
)
# Will look for myapp.toml instead of general.toml
config = globals_dto.configuration
print( f"Configuration loaded from custom template: {dict( config )}" )
if __name__ == '__main__':
asyncio.run( main( ) )
For testing, you can create custom acquirers that return specific configuration data:
>>> import appcore
>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class TestConfigurationAcquirer:
... ''' Custom acquirer for testing. '''
... config_data: dict
...
... async def __call__( self, *args, **kwargs ):
... return appcore.accretive.Dictionary( self.config_data )
>>>
>>> # Create test acquirer with specific data
>>> test_data = { 'application': { 'name': 'test-app', 'debug': True } }
>>> test_acquirer = TestConfigurationAcquirer( config_data = test_data )
>>> test_acquirer.config_data[ 'application' ][ 'debug' ]
True
Advanced Configuration Patterns¶
Complex applications can use sophisticated configuration patterns:
import asyncio
import contextlib
import os
import appcore
def apply_environment_overrides( config ):
''' Apply environment variable overrides. '''
# Override debug mode from environment
if 'DEBUG' in os.environ:
if 'application' not in config:
config[ 'application' ] = { }
config[ 'application' ][ 'debug' ] = (
os.environ[ 'DEBUG' ].lower( ) in ( 'true', '1', 'yes' ) )
# Override log level from environment
if 'LOG_LEVEL' in os.environ:
if 'logging' not in config:
config[ 'logging' ] = { }
config[ 'logging' ][ 'level' ] = os.environ[ 'LOG_LEVEL' ]
async def main( ):
# Combine multiple configuration sources
edits = ( apply_environment_overrides, )
async with contextlib.AsyncExitStack( ) as exits:
globals_dto = await appcore.prepare(
exits,
configedits = edits,
environment = True # Also load .env files
)
config = globals_dto.configuration
print( f"Final config: {dict( config )}" )
if __name__ == '__main__':
asyncio.run( main( ) )
Resource Management Patterns¶
Advanced patterns for managing resources with AsyncExitStack:
>>> import tempfile
>>> import io
>>> import platformdirs
>>> import contextlib
>>> import appcore
>>>
>>> async def example_with_temporary_resources( ):
... ''' Example using temporary resources that are cleaned up automatically. '''
... async with contextlib.AsyncExitStack( ) as exits:
... # Create temporary directory
... temp_dir = exits.enter_context( tempfile.TemporaryDirectory( ) )
... print( f"Created temp directory: {temp_dir}" )
... # Use custom directories pointing to temp location
... custom_dirs = platformdirs.PlatformDirs( 'temp-app', ensure_exists = False )
... # Initialize appcore with temporary resources
... globals_dto = await appcore.prepare(
... exits,
... directories = custom_dirs,
... configfile = io.StringIO( '''
... [application]
... name = "temp-app"
... [data]
... temporary = true
... ''' )
... )
... # Use the globals object
... config = globals_dto.configuration
... is_temporary = config[ 'data' ][ 'temporary' ]
... print( f"Using temporary setup: {is_temporary}" )
... return globals_dto
... # temp_dir is automatically cleaned up when exiting the context
This pattern is particularly useful for testing scenarios where you need isolated, temporary resources that are guaranteed to be cleaned up.
Next Steps¶
This covers configuration management in appcore. For more topics, see:
Environment Handling - Environment variables and development detection
Basic Usage - Application setup and platform directories