Basic Usage

Introduction

The appcore package provides a simple async initialization framework for applications that need configuration loading, platform directory management, and environment setup. The core function is prepare(), which returns a Globals object containing all the initialized application state.

Basic Application Setup

The simplest way to initialize appcore for your application:

import asyncio
import contextlib
import appcore

async def main( ):
    async with contextlib.AsyncExitStack( ) as exits:
        # Initialize application core with sensible defaults
        globals_dto = await appcore.prepare( exits )
        # Access application information
        print( f"Application: {globals_dto.application.name}" )
        print( f"Publisher: {globals_dto.application.publisher}" )
        print( f"Version: {globals_dto.application.version}" )
        # Check if running in development mode
        if globals_dto.distribution.editable:
            print( 'Running in development mode' )
        else:
            print( 'Running in production mode' )

if __name__ == '__main__':
    asyncio.run( main( ) )

This will automatically detect your application name from the package, set up platform directories, load configuration, and prepare logging.

Platform Directory Access

The Globals object provides convenient access to platform-specific directories for your application:

import asyncio
import contextlib
import appcore
from pathlib import Path

async def main( ):
    async with contextlib.AsyncExitStack( ) as exits:
        globals_dto = await appcore.prepare( exits )
        # Get platform directories for data storage
        cache_dir = globals_dto.provide_cache_location( )
        data_dir = globals_dto.provide_data_location( )
        state_dir = globals_dto.provide_state_location( )
        print( f"Cache directory: {cache_dir}" )
        print( f"Data directory: {data_dir}" )
        print( f"State directory: {state_dir}" )
        # Create subdirectories for organization
        logs_dir = globals_dto.provide_data_location( 'logs' )
        config_dir = globals_dto.provide_data_location( 'config' )
        print( f"Logs will go in: {logs_dir}" )
        print( f"Config files in: {config_dir}" )
        # Directories are automatically created as needed
        logs_dir.mkdir( parents = True, exist_ok = True )
        config_dir.mkdir( parents = True, exist_ok = True )

if __name__ == '__main__':
    asyncio.run( main( ) )

The exact paths depend on your operating system:

  • Linux: ~/.cache/appname, ~/.local/share/appname, ~/.local/state/appname

  • macOS: ~/Library/Caches/appname, ~/Library/Application Support/appname, etc.

  • Windows: %LOCALAPPDATA%\appname, %APPDATA%\appname, etc.

Working with Application Data

You can store and retrieve application data using the provided directories:

import asyncio
import contextlib
import json
import appcore

async def save_user_preferences( globals_dto, preferences ):
    ''' Save user preferences to the data directory. '''
    prefs_file = globals_dto.provide_data_location( 'preferences.json' )
    prefs_file.parent.mkdir( parents = True, exist_ok = True )
    with open( prefs_file, 'w' ) as f:
        json.dump( preferences, f, indent = 2 )
    print( f"Preferences saved to: {prefs_file}" )

async def load_user_preferences( globals_dto ):
    ''' Load user preferences from the data directory. '''
    prefs_file = globals_dto.provide_data_location( 'preferences.json' )
    if prefs_file.exists( ):
        with open( prefs_file, 'r' ) as f:
            preferences = json.load( f )
        print( f"Loaded preferences: {preferences}" )
        return preferences
    else:
        print( 'No preferences file found, using defaults' )
        return { 'theme': 'dark', 'auto_save': True }

async def main( ):
    async with contextlib.AsyncExitStack( ) as exits:
        globals_dto = await appcore.prepare( exits )
        # Save some example preferences
        prefs = { 'theme': 'light', 'auto_save': False, 'recent_files': [ ] }
        await save_user_preferences( globals_dto, prefs )
        # Load them back
        loaded_prefs = await load_user_preferences( globals_dto )

if __name__ == '__main__':
    asyncio.run( main( ) )

Custom Application Information

You can customize the application metadata used for directory generation:

import asyncio
import contextlib
import appcore

async def main( ):
    # Create custom application information
    app_info = appcore.ApplicationInformation(
        name = 'my-awesome-app',
        publisher = 'MyCompany',
        version = '2.1.0'
    )
    async with contextlib.AsyncExitStack( ) as exits:
        # Use custom application info
        globals_dto = await appcore.prepare(
            exits,
            application = app_info
        )
        print( f"App: {globals_dto.application.name}" )
        print( f"Publisher: {globals_dto.application.publisher}" )
        print( f"Data dir: {globals_dto.provide_data_location()}" )
        # The directories will include publisher and version info:
        # Linux: ~/.local/share/MyCompany/my-awesome-app/2.1.0/
        # macOS: ~/Library/Application Support/MyCompany/my-awesome-app/2.1.0/

if __name__ == '__main__':
    asyncio.run( main( ) )

Error Handling

The prepare() function can raise exceptions in certain scenarios. Use Omnierror to catch all package errors, or specific exceptions for fine-grained handling:

import asyncio
import contextlib
import appcore

async def main( ):
    try:
        async with contextlib.AsyncExitStack( ) as exits:
            globals_dto = await appcore.prepare( exits )
            print( 'Initialization successful!' )
    except appcore.exceptions.Omnierror as e:
        print( f"Appcore initialization error: {e}" )
        # Omnierror catches all errors from the package API
        # You can also catch specific exceptions for fine-grained handling
    except Exception as e:
        print( f"Unexpected system error: {e}" )
        raise

if __name__ == '__main__':
    asyncio.run( main( ) )

For fine-grained error handling, catch specific exceptions:

try:
    async with contextlib.AsyncExitStack( ) as exits:
        globals_dto = await appcore.prepare( exits )
except appcore.exceptions.FileLocateFailure as e:
    print( f"Could not locate required files: {e}" )
    # This can happen in development if pyproject.toml is not found
except appcore.exceptions.OperationInvalidity as e:
    print( f"Invalid operation during setup: {e}" )
    # This can happen with malformed configuration files

Logging Configuration

The appcore package provides flexible logging configuration through the inscription module. Logging is automatically configured during prepare() with sensible defaults, but can be customized for different environments.

Basic Logging Setup

Logging is configured automatically with the default settings:

import asyncio
import contextlib
import logging
import appcore

async def main( ):
    async with contextlib.AsyncExitStack( ) as exits:
        # Logging is configured automatically with defaults
        globals_dto = await appcore.prepare( exits )

        # Use standard Python logging
        logger = logging.getLogger( __name__ )
        logger.info( 'Application started successfully' )
        logger.debug( 'Debug information' )
        logger.warning( 'Warning message' )
        logger.error( 'Error occurred' )

if __name__ == '__main__':
    asyncio.run( main( ) )

The default configuration uses:

  • Mode: Plain (standard Python logging)

  • Level: Info (shows info, warning, error, critical)

  • Target: Standard error stream

Custom Logging Configuration

You can customize logging behavior using inscription.Control:

import asyncio
import contextlib
import logging
import sys
import appcore

async def main( ):
    # Create custom inscription control
    inscription_control = appcore.inscription.Control(
        mode = appcore.inscription.Modes.Rich,  # Enhanced formatting
        level = 'debug',  # Show debug messages
        target = sys.stdout  # Log to stdout instead of stderr
    )

    async with contextlib.AsyncExitStack( ) as exits:
        globals_dto = await appcore.prepare(
            exits,
            inscription = inscription_control
        )

        # Test different log levels
        logger = logging.getLogger( __name__ )
        logger.debug( 'Debug: Application initialized' )
        logger.info( 'Info: Processing started' )
        logger.warning( 'Warning: Non-critical issue detected' )
        logger.error( 'Error: Something went wrong' )

if __name__ == '__main__':
    asyncio.run( main( ) )

Logging Modes

The inscription module supports three different modes:

Null Mode - Minimal logging setup, defers to external management:

inscription_control = appcore.inscription.Control(
    mode = appcore.inscription.Modes.Null
)

Plain Mode - Standard Python logging (default):

inscription_control = appcore.inscription.Control(
    mode = appcore.inscription.Modes.Plain,
    level = 'info'
)

Rich Mode - Enhanced formatting with colors and better tracebacks:

inscription_control = appcore.inscription.Control(
    mode = appcore.inscription.Modes.Rich,
    level = 'debug'
)

Rich mode automatically falls back to plain mode if the rich package is not installed.

Environment Variable Configuration

Logging levels can be controlled through environment variables without code changes:

# Set inscription level specifically (using your application name)
export MYAPP_INSCRIPTION_LEVEL=debug

# Or use the general log level variable
export MYAPP_LOG_LEVEL=warning
import asyncio
import contextlib
import logging
import appcore

async def main( ):
    async with contextlib.AsyncExitStack( ) as exits:
        # Environment variables automatically override configured level
        globals_dto = await appcore.prepare( exits )

        logger = logging.getLogger( __name__ )
        logger.debug( 'This appears if MYAPP_INSCRIPTION_LEVEL=debug' )
        logger.info( 'This appears if level is info or lower' )
        logger.warning( 'This appears if level is warning or lower' )

if __name__ == '__main__':
    asyncio.run( main( ) )

Environment variable precedence:

  1. {APPLICATION_NAME}_INSCRIPTION_LEVEL - Inscription-specific level (highest priority)

  2. {APPLICATION_NAME}_LOG_LEVEL - General log level

  3. Configuration in code (lowest priority)

Where {APPLICATION_NAME} is your application’s name in uppercase (e.g., MYAPP_INSCRIPTION_LEVEL for an application named “myapp”).

Custom Log Targets

You can redirect logs to custom streams or files:

import asyncio
import contextlib
import logging
import io
import appcore

async def main( ):
    # Create custom output stream
    log_stream = io.StringIO( )

    inscription_control = appcore.inscription.Control(
        mode = appcore.inscription.Modes.Plain,
        level = 'info',
        target = log_stream
    )

    async with contextlib.AsyncExitStack( ) as exits:
        globals_dto = await appcore.prepare(
            exits,
            inscription = inscription_control
        )

        # Log some messages
        logger = logging.getLogger( __name__ )
        logger.info( 'Message 1' )
        logger.info( 'Message 2' )
        logger.warning( 'Warning message' )

        # Retrieve captured logs
        log_output = log_stream.getvalue( )
        print( f"Captured logs:\n{log_output}" )

if __name__ == '__main__':
    asyncio.run( main( ) )

This is particularly useful for testing or when you need to capture and process log output programmatically.

Application Profiles for Different Deployments

Create application information objects for different deployment scenarios:

>>> import appcore
>>> def create_application_profiles( ):
...     ''' Create different application profiles. '''
...     return {
...         'development': appcore.ApplicationInformation(
...             name = 'myapp-dev',
...             publisher = 'DevCorp',
...             version = '0.1.0-dev'
...         ),
...         'staging': appcore.ApplicationInformation(
...             name = 'myapp-staging',
...             publisher = 'DevCorp',
...             version = '1.0.0-rc1'
...         ),
...         'production': appcore.ApplicationInformation(
...             name = 'myapp',
...             publisher = 'ProductionCorp',
...             version = '1.0.0'
...         )
...     }
>>>
>>> profiles = create_application_profiles( )
>>> dev_profile = profiles[ 'development' ]
>>> print( f"Dev app: {dev_profile.name} v{dev_profile.version}" )
Dev app: myapp-dev v0.1.0-dev
>>>
>>> # Each profile will have different platform directories
>>> dev_dirs = dev_profile.produce_platform_directories( )
>>> prod_dirs = profiles[ 'production' ].produce_platform_directories( )
>>> print( f"Different apps use different directories" )
Different apps use different directories

This pattern is useful for applications that need different configurations for development, staging, and production environments.

Next Steps

This covers the basic usage of appcore. For more advanced topics, see:

  • Configuration Management - Loading TOML configuration files with includes

  • Environment Handling - Development vs production detection and environment variables