Full Python API reference for lib_layered_config. For the overview and installation see
the README.
from lib_layered_config import (
Config,
Layer,
read_config,
read_config_json,
read_config_raw,
default_env_prefix,
deploy_config,
generate_examples,
i_should_fail,
# Profile validation
DEFAULT_MAX_PROFILE_LENGTH,
validate_profile_name,
is_valid_profile_name,
# Permission constants (for deploy_config)
DEFAULT_APP_DIR_MODE,
DEFAULT_APP_FILE_MODE,
DEFAULT_USER_DIR_MODE,
DEFAULT_USER_FILE_MODE,
# Deploy permission types (for deploy_config's dir_mode/file_mode/permissions/permission_overrides)
DeployMode,
ModeKind,
DeployModeError,
LayerModes,
DeployPermissions,
DeployPermissionsError,
PermissionProblem,
deploy_permissions_from_config,
)The Layer enum provides type-safe constants for configuration layer names:
from lib_layered_config import Layer
# Available layers (in precedence order, lowest to highest):
Layer.DEFAULTS # "defaults" - bundled application defaults
Layer.APP # "app" - system-wide application config
Layer.HOST # "host" - machine-specific overrides
Layer.USER # "user" - per-user preferences
Layer.DOTENV # "dotenv" - project-local .env file
Layer.ENV # "env" - environment variables (highest precedence)
# Layer values are strings, so they work seamlessly with provenance:
origin = config.origin("service.timeout")
if origin and origin["layer"] == Layer.ENV:
print("Value comes from environment variable")Immutable configuration value object with provenance tracking and dotted-path lookups.
Return the value for a dotted key path or a default when the path is missing.
Parameters:
key(str, required): Dotted path identifying nested entries (e.g.,"service.timeout"or"db.host").default(Any, optional): Value to return when the path does not resolve or encounters a non-mapping. Default:None.
Returns: The resolved value or default when missing.
Examples:
Example 1: Basic dotted-path lookup
from lib_layered_config import read_config
# Load configuration
config = read_config(vendor="Acme", app="Demo", slug="demo")
# Access nested configuration values using dotted paths
timeout = config.get("service.timeout", default=30)
endpoint = config.get("service.endpoint")
db_host = config.get("database.host", default="localhost")
print(f"Service timeout: {timeout}s")
print(f"Service endpoint: {endpoint}")
print(f"Database host: {db_host}")Explanation: The get method traverses nested dictionaries using dot notation. If service.timeout exists in your configuration, it returns that value; otherwise, it returns the default (30).
Example 2: Handling missing keys gracefully
# This returns None if the key doesn't exist
api_key = config.get("api.secret_key")
if api_key is None:
print("Warning: API key not configured")
# This returns a default value
max_retries = config.get("api.max_retries", default=3)
print(f"Max retries: {max_retries}")Explanation: When you don't provide a default, get returns None for missing keys. This is useful for optional configuration values where you need to check if they were configured.
Example 3: Deep nested paths
# Access deeply nested configuration
smtp_host = config.get("email.smtp.host", default="smtp.gmail.com")
smtp_port = config.get("email.smtp.port", default=587)
use_tls = config.get("email.smtp.tls.enabled", default=True)
print(f"SMTP: {smtp_host}:{smtp_port} (TLS: {use_tls})")Explanation: The dotted path can be arbitrarily deep. If any intermediate key is missing or not a dictionary, the default value is returned.
Return provenance metadata for a dotted key when available.
Parameters:
key(str, required): Dotted key in the metadata map (e.g.,"service.timeout").
Returns: Dictionary with keys layer (str), path (str | None), and key (str), or None if the key was never observed. A table that holds values has no entry of its own (its values carry theirs), so origin() returns None for it; an empty table reports where it was last declared.
Examples:
Example 1: Check where a value came from
from lib_layered_config import read_config
config = read_config(vendor="Acme", app="Demo", slug="demo")
# Get provenance information
timeout_origin = config.origin("service.timeout")
if timeout_origin:
print(f"service.timeout = {config.get('service.timeout')}")
print(f" Layer: {timeout_origin['layer']}")
print(f" Source: {timeout_origin['path'] or 'environment variable'}")
print(f" Key: {timeout_origin['key']}")
# Output example:
# service.timeout = 30
# Layer: env
# Source: environment variable
# Key: service.timeoutExplanation: The origin method tells you which configuration layer provided a value. This is crucial for debugging when you need to understand why a particular value is being used.
Example 2: Debugging configuration precedence
# Check multiple values to understand the configuration hierarchy
keys_to_check = ["database.host", "database.port", "service.timeout"]
for key in keys_to_check:
value = config.get(key)
origin = config.origin(key)
if origin:
layer = origin["layer"]
source = origin["path"] or "(ephemeral)"
print(f"{key}: {value} [from {layer}] {source}")
else:
print(f"{key}: Not configured")
# Output example:
# database.host: localhost [from user] /home/alice/.config/demo/config.toml
# database.port: 5432 [from app] /etc/demo/config.toml
# service.timeout: 30 [from env] (ephemeral)Explanation: This shows how to audit your entire configuration to see which layer each value came from. Useful when troubleshooting unexpected configuration values.
Example 3: Validate configuration source for security
# Ensure sensitive values come from environment or dotenv
sensitive_keys = ["api.secret_key", "database.password"]
for key in sensitive_keys:
origin = config.origin(key)
if origin:
if origin["layer"] not in ["env", "dotenv"]:
print(f"WARNING: {key} should come from env/dotenv, not {origin['layer']}")
print(f" Currently in: {origin['path']}")
else:
print(f"ERROR: {key} is not configured!")
# This helps ensure secrets aren't committed to config filesExplanation: You can use provenance to enforce security policies, ensuring sensitive values only come from appropriate sources (environment variables or .env files, not checked-in config files).
Return a deep, mutable copy of the configuration tree.
Parameters:
redact(bool, optional): Replace the values of sensitive keys with***REDACTED***; keys declared throughread_config(not_sensitive=...)stay visible. Default:False.
Returns: Dictionary containing a deep copy of all configuration data.
Examples:
Example 1: Export configuration for serialization
from lib_layered_config import read_config
import json
config = read_config(vendor="Acme", app="Demo", slug="demo")
# Get a mutable copy of the entire configuration
data = config.as_dict()
# Now you can serialize it however you want
with open("config-snapshot.json", "w") as f:
json.dump(data, f, indent=2)
print("Configuration exported to config-snapshot.json")Explanation: Use as_dict() when you need to export or serialize the configuration data. The returned dictionary is completely independent from the original Config object.
Example 2: Modify configuration copy for testing
# Create a modified copy for testing without affecting the original
test_config = config.as_dict()
test_config["database"]["host"] = "test-db.example.com"
test_config["service"]["timeout"] = 1 # Short timeout for tests
# Original config is unchanged
print(f"Original DB: {config.get('database.host')}") # localhost
print(f"Test DB: {test_config['database']['host']}") # test-db.example.comExplanation: This is useful in tests where you want to create variations of your configuration without modifying the immutable Config object.
Return the value at a dotted key as as_dict(redact=True) shows it, for a display that shows one key or section. A key below a masked section is masked too, so a single key never shows what the whole view hides; keys declared not sensitive stay visible.
Parameters:
key(str, required): Dotted path identifying nested entries.default(Any, optional): Value returned whenkeydoes not exist. Default:None.
Returns: The redacted value, or default.
Example:
from lib_layered_config.domain.config import Config
config = Config({"credentials": {"user": "alice"}, "db": {"host": "h"}}, {})
print(config.get_redacted("credentials.user")) # ***REDACTED***
print(config.get_redacted("db.host")) # h
print(config.get_redacted("db.port", 0)) # 0Serialize the configuration as JSON.
Parameters:
indent(int | None, optional): Indentation level for pretty-printing.Noneproduces compact output. Default:None.redact(bool, optional): Replace the values of sensitive keys with***REDACTED***; keys declared throughread_config(not_sensitive=...)stay visible. Default:False.
Returns: JSON string containing the configuration data.
Examples:
Example 1: Pretty-printed JSON for logs
from lib_layered_config import read_config
config = read_config(vendor="Acme", app="Demo", slug="demo")
# Pretty-printed JSON with 2-space indentation
pretty_json = config.to_json(indent=2)
print("Current configuration:")
print(pretty_json)
# Output:
# {
# "service": {
# "timeout": 30,
# "endpoint": "https://api.example.com"
# },
# "database": {
# "host": "localhost"
# }
# }Explanation: Use indent=2 or indent=4 for human-readable JSON output, perfect for logging or debugging.
Example 2: Compact JSON for APIs or storage
# Compact JSON (no whitespace)
compact_json = config.to_json()
print(compact_json)
# Output: {"service":{"timeout":30,"endpoint":"https://api.example.com"},...}
# This is useful when sending config over the network or storing in databasesExplanation: Compact JSON (no indent) minimizes the payload size, useful for network transmission or storage.
Return a new configuration with overrides deep-merged in: nested tables merge key by key, while scalars and lists replace the value they meet.
Parameters:
overrides(Mapping[str, Any], required): Nested mapping of keys and values to merge in.
Returns: New Config instance with overrides applied. Every key the override supplies reports origin() as layer "override" (OVERRIDE_LAYER), path None; keys it did not touch keep their original source, and keys it removed have none. The original instance is unchanged.
Examples:
Example 1: Override configuration for specific environment
from lib_layered_config import read_config
# Load base configuration
config = read_config(vendor="Acme", app="Demo", slug="demo")
# Create a version with production overrides
prod_config = config.with_overrides(
{
"service": {"endpoint": "https://prod-api.example.com", "timeout": 60},
"database": {"host": "prod-db.example.com", "pool_size": 100},
}
)
print(f"Dev endpoint: {config.get('service.endpoint')}")
print(f"Prod endpoint: {prod_config.get('service.endpoint')}")
# Original config is unchangedExplanation: This allows you to create environment-specific configurations from a base configuration without mutating the original.
Example 2: Testing with feature flags
# Enable feature flags for testing
test_config = config.with_overrides({"features": {"new_ui": True, "experimental_api": True, "debug_mode": True}})
# Use test_config in your tests
if test_config.get("features.new_ui"):
print("Running tests with new UI enabled")Explanation: Great for testing different configurations or feature flag combinations without modifying files or environment variables.
Access top-level keys directly using bracket notation.
Parameters:
key(str): Top-level key to retrieve.
Returns: Stored value.
Raises: KeyError when key does not exist.
Examples:
Example 1: Direct access to top-level keys
from lib_layered_config import read_config
config = read_config(vendor="Acme", app="Demo", slug="demo")
# Access top-level sections directly
service_config = config["service"]
database_config = config["database"]
print(f"Service section: {service_config}")
# Output: {'timeout': 30, 'endpoint': 'https://api.example.com'}
print(f"DB host: {database_config['host']}")
# Output: localhostExplanation: Use bracket notation config[key] to access top-level configuration sections. This returns the full nested dictionary for that section.
Example 2: Iterate over configuration sections
# Iterate over all top-level configuration keys
for section in config:
print(f"Section: {section}")
print(f" Keys: {list(config[section].keys())}")
# Output:
# Section: service
# Keys: ['timeout', 'endpoint']
# Section: database
# Keys: ['host', 'port']Explanation: Since Config implements the Mapping protocol, you can iterate over it like a dictionary to discover all configured sections.
Load and merge all configuration layers into an immutable Config object with provenance metadata.
Parameters:
vendor(str, required): Vendor namespace used to compute filesystem paths (e.g.,"Acme").app(str, required): Application name used to compute filesystem paths (e.g.,"ConfigKit").slug(str, required): Configuration slug used for file paths and environment variable prefix (e.g.,"config-kit").profile(str | None, optional): Configuration profile name (e.g.,"test","production"). When specified, adds aprofile/<name>/segment to all configuration paths. Default:None(no profile).prefer(Sequence[str] | None, optional): Ordered sequence of preferred file suffixes (e.g.,["toml", "json", "yaml"]). Files matching earlier suffixes take precedence. Default:None(accepts all supported formats with default ordering).start_dir(str | Path | None, optional): Starting directory for upward.envfile search; a directory the search cannot enter is skipped. Default:None(uses current working directory; when the operating system cannot name it - a removed directory, or on macOS one the user cannot enter - no project.envis searched).default_file(str | Path | None, optional): Path to a file injected as the lowest-precedence layer (loaded before app/host/user layers). Default:None(no defaults layer).dotenv_path(str | Path | None, optional): Explicit path to a.envfile. When set, this file is loaded directly instead of searching upward fromstart_dir. Default:None(use directory search).not_sensitive(Iterable[str] | None, optional): Dotted key paths you declare as not secret although their name looks like one (e.g.["performance.max_token_bytes"]). Redaction leaves them visible, and an unquotednull/nonein the environment or.envreads asNonefor them. The returnedConfigcarries the declaration. A barestror a non-strentry raisesTypeError; an empty segment ("a..b") raisesValueError. Default:None.
Returns: Immutable Config object with merged configuration and provenance tracking.
Raises: LayerLoadError (a ConfigError) when a layer file cannot be decoded or parsed: "<path> is not valid <FORMAT>", or "<path> nests deeper than the <FORMAT> parser allows" when the parser stopped at its
nesting limit (TOML 80 levels, JSON 1024, YAML Python's recursion limit), plus " (line N, column M)" when the
parser reports a position; "<path> is not valid UTF-8 (line N, byte offset M)" for an undecodable file, or "<path> is not valid UTF-16|UTF-32 (byte offset M)" for
a BOM-marked YAML file that does not decode; "<path> has keys that are not strings (quote them): <parent>: <key> (<type>)" for a YAML mapping key such as an unquoted 1: or true:; "<path> uses !!omap or !!pairs, whose keys redaction cannot see; write a plain mapping instead: <position>[; <position>...]"
for a YAML !!omap or !!pairs value with entries (an empty one loads); "<path> uses !!set or !!binary, which have no JSON form; write a list or a base64 string instead: <position> (<type>)[; ...]" for a YAML
!!set or !!binary value; "<path> is a directory, not a configuration file" when default_file, a discovered config path or dotenv_path names a directory;
"<path> cannot be read: <reason>" when a layer's config location, the platform .env location, default_file or
dotenv_path sits behind a permission wall (a directory on its path the user cannot enter, or a file the user
cannot read) or is a name the operating system cannot take (a NUL byte). With dotenv_path set, the search
and its platform .env location are never probed. The upward
.env search is the exception: a directory it cannot enter holds no .env for it and is skipped. None of
these quotes the file's content.
Examples:
Example 1: Basic usage - Load configuration with defaults
from lib_layered_config import read_config
# Simplest usage - just specify your app identity
config = read_config(vendor="Acme", app="MyApp", slug="myapp")
# Access configuration values
timeout = config.get("service.timeout", default=30)
endpoint = config.get("service.endpoint", default="https://api.example.com")
print(f"Service will connect to {endpoint} with {timeout}s timeout")Explanation: This is the minimal setup. The library will automatically look for configuration files in standard locations (/etc/myapp/, ~/.config/myapp/, etc.) and merge them with environment variables.
Example 2: Using file format preferences
from lib_layered_config import read_config
# Prefer TOML files over JSON when both exist
config = read_config(vendor="Acme", app="MyApp", slug="myapp", prefer=["toml", "json", "yaml"])
# If both config.toml and config.json exist in the same directory,
# config.toml will be loaded because it appears first in the prefer listExplanation: The prefer parameter controls which file format takes precedence when multiple formats exist in the same directory. This is useful when migrating from one format to another.
Example 3: Using a defaults file
from pathlib import Path
from lib_layered_config import read_config
# Start with application defaults before applying environment-specific overrides
config = read_config(vendor="Acme", app="MyApp", slug="myapp", default_file=Path("./config/defaults.toml"))
# Precedence order now becomes:
# 1. defaults.toml (lowest)
# 2. /etc/myapp/config.toml (app layer)
# 3. /etc/myapp/hosts/hostname.toml (host layer)
# 4. ~/.config/myapp/config.toml (user layer)
# 5. .env files (dotenv layer)
# 6. Environment variables (highest)Explanation: Use default_file to ship reasonable defaults with your application that can be overridden by system admins (app layer), per-machine configs (host layer), or users.
Example 4: Project-specific .env search
from pathlib import Path
from lib_layered_config import read_config
# Specify where to start searching for .env files
project_root = Path(__file__).parent.parent
config = read_config(vendor="Acme", app="MyApp", slug="myapp", start_dir=str(project_root))
# The library will search for .env files starting from project_root
# and moving upward through parent directoriesExplanation: Use start_dir to control where .env file discovery begins. This ensures your project's .env file is found even if your script runs from a subdirectory.
Example 5: Explicit .env file path
from pathlib import Path
from lib_layered_config import read_config
# Load a specific .env file instead of searching upward
config = read_config(vendor="Acme", app="MyApp", slug="myapp", dotenv_path=Path("/opt/myapp/secrets/.env.production"))
# The library loads /opt/myapp/secrets/.env.production directly
# without searching parent directories for .env filesExplanation: Use dotenv_path when you know the exact location of the .env file. This is common in containers and CI/CD where secrets are mounted at a known path.
Example 6: Complete setup with all parameters
from pathlib import Path
from lib_layered_config import read_config
# Production-ready configuration loading
config = read_config(
vendor="Acme",
app="MyApp",
slug="myapp",
prefer=["toml", "json"], # TOML preferred
start_dir=Path.cwd(), # Search .env from current directory
default_file=Path(__file__).parent / "defaults.toml", # Ship defaults
)
# Use the configuration
db_host = config.get("database.host", default="localhost")
db_port = config.get("database.port", default=5432)
db_name = config.get("database.name", default="myapp")
print(f"Connecting to PostgreSQL at {db_host}:{db_port}/{db_name}")
# Check where each value came from
for key in ["database.host", "database.port", "database.name"]:
origin = config.origin(key)
if origin:
print(f" {key}: from {origin['layer']} layer")Explanation: This complete example shows production-ready configuration loading with defaults, format preferences, and provenance tracking for debugging.
Load configuration and return it as JSON with provenance metadata.
Parameters:
vendor(str, required): Vendor namespace.app(str, required): Application name.slug(str, required): Configuration slug.profile(str | None, optional): Configuration profile name. Addsprofile/<name>/to paths. Default:None.prefer(Sequence[str] | None, optional): Ordered sequence of preferred file suffixes. Default:None.start_dir(str | Path | None, optional): Starting directory for.envsearch. Default:None.default_file(str | Path | None, optional): Path to lowest-precedence defaults file. Default:None.dotenv_path(str | Path | None, optional): Explicit.envfile path (skips upward search). Default:None.indent(int | None, optional): JSON indentation level.Nonefor compact output. Default:None.redact(bool, optional): Replace the values of sensitive keys (password,token,secret,*_key, ...) with***REDACTED***in the config data. Default:False.not_sensitive(Iterable[str] | None, optional): Dotted key paths you declare as not secret although their name looks like one (e.g.["performance.max_token_bytes"]). Redaction leaves them visible, and an unquotednull/nonein the environment or.envreads asNonefor them. Declared keys stay visible whenredactis set. A barestror a non-strentry raisesTypeError; an empty segment ("a..b") raisesValueError. Default:None.
Returns: JSON string containing {"config": {...}, "provenance": {...}}.
Examples:
Example 1: API endpoint - Return configuration as JSON
from lib_layered_config import read_config_json
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/api/config")
def get_config():
# Load and return configuration as JSON with provenance
json_payload = read_config_json(
vendor="Acme",
app="MyApp",
slug="myapp",
indent=2, # Pretty-printed for readability
)
return json_payload, 200, {"Content-Type": "application/json"}
# The response includes both config values and their sourcesExplanation: Perfect for exposing configuration through APIs. The JSON includes provenance data so clients can see where each value came from.
Example 2: Configuration audit tool
from lib_layered_config import read_config_json
import json
# Load configuration with provenance
payload = read_config_json(vendor="Acme", app="MyApp", slug="myapp", indent=2)
data = json.loads(payload)
# Audit where sensitive values come from
print("Configuration Audit Report")
print("=" * 50)
for key, info in data["provenance"].items():
value = data["config"]
# Navigate to the value using the key
for part in key.split("."):
value = value.get(part, {})
print(f"\n{key}: {value}")
print(f" Source Layer: {info['layer']}")
print(f" File Path: {info['path'] or '(environment variable)'}")Explanation: Use this for creating audit reports that show exactly where each configuration value originated from.
Example 3: Compact JSON for logging
from lib_layered_config import read_config_json
import logging
# Get compact JSON (no indentation) for structured logging
compact_json = read_config_json(
vendor="Acme",
app="MyApp",
slug="myapp",
indent=None, # Compact output
)
# Log the configuration snapshot
logging.info(f"Application started with config: {compact_json}")Explanation: Compact JSON is ideal for log aggregation systems where you want to log the entire configuration as a single line.
Return raw data and provenance mappings for advanced tooling.
Parameters:
vendor(str, required): Vendor namespace.app(str, required): Application name.slug(str, required): Configuration slug.profile(str | None, optional): Configuration profile name. Addsprofile/<name>/to paths. Default:None.prefer(Sequence[str] | None, optional): Ordered sequence of preferred file suffixes. Default:None.start_dir(str | None, optional): Starting directory for.envsearch. Default:None.default_file(str | Path | None, optional): Path to lowest-precedence defaults file. Default:None.dotenv_path(str | Path | None, optional): Explicit.envfile path (skips upward search). Default:None.not_sensitive(Iterable[str] | None, optional): Dotted key paths you declare as not secret although their name looks like one (e.g.["performance.max_token_bytes"]). An unquotednull/nonein the environment or.envreads asNonefor them. The returned plain dicts do not carry the declaration: pass it again asredact_mapping(data, not_sensitive=...)when you redact them. A barestror a non-strentry raisesTypeError; an empty segment ("a..b") raisesValueError. Default:None.
Returns: Tuple of (data_dict, provenance_dict) where both are mutable dictionaries.
Examples:
Example 1: Template rendering with configuration
from lib_layered_config import read_config_raw
from jinja2 import Template
# Load configuration as raw dictionaries
data, provenance = read_config_raw(vendor="Acme", app="MyApp", slug="myapp")
# Use in template rendering
template = Template("""
Database Configuration:
Host: {{ database.host }}
Port: {{ database.port }}
Database: {{ database.name }}
Service Configuration:
Timeout: {{ service.timeout }}s
Endpoint: {{ service.endpoint }}
""")
output = template.render(**data)
print(output)Explanation: Raw dictionaries are perfect for template rendering where you need mutable data structures.
Example 2: Configuration validation
from lib_layered_config import read_config_raw
# Load configuration
data, provenance = read_config_raw(vendor="Acme", app="MyApp", slug="myapp")
# Validate required fields
required_keys = [
("database.host", str),
("database.port", int),
("service.timeout", int),
]
errors = []
for key, expected_type in required_keys:
# Navigate the nested dictionary
value = data
for part in key.split("."):
value = value.get(part) if isinstance(value, dict) else None
if value is None:
break
if value is None:
errors.append(f"Missing required key: {key}")
elif not isinstance(value, expected_type):
errors.append(f"{key} must be {expected_type.__name__}, got {type(value).__name__}")
if errors:
print("Configuration validation errors:")
for error in errors:
print(f" - {error}")
else:
print("Configuration is valid!")Explanation: Use read_config_raw for advanced validation or transformation where you need full control over the data structures.
Example 3: Merge with runtime overrides
from lib_layered_config import read_config_raw
# Load base configuration
data, provenance = read_config_raw(vendor="Acme", app="MyApp", slug="myapp")
# Apply runtime overrides (e.g., from command-line arguments)
if args.db_host:
data["database"]["host"] = args.db_host
if args.debug:
data["logging"]["level"] = "DEBUG"
# Now use the modified configuration
print(f"Final configuration: {data}")Explanation: Raw dictionaries can be mutated, making them useful when you need to apply runtime overrides from command-line arguments or other sources.
is_sensitive(name) decides by key name alone, so a size such as max_token_bytes is masked like a token.
NotSensitiveKeys lets an application declare such keys; the helpers below are importable from lib_layered_config.
Build the declaration from dotted paths (matched case-insensitively; an item in a list is addressed by the
list's own path, with no index). Pass None for none (returns NotSensitiveKeys.NONE) or an existing instance
(returned as is). It covers its own path only: keys below it are still checked, and a masked section stays masked
unless the section itself is declared. covers(path) returns whether a sequence of segments was declared;
iterating an instance yields the declared dotted paths, sorted.
Raises: TypeError for a single str or a non-str entry, and ValueError for an empty segment. covers
refuses a str path with TypeError, as does NotSensitiveKeys(paths) built directly from anything of would not produce
(ValueError for an empty or non-lowercase segment).
Return whether the key at path is masked: its own name looks secret and nothing declared it.
Parameters:
path(Sequence[str], required): The key's segments from the root, its own name last. A dottedstrraisesTypeError; writetuple("db.password".split(".")).not_sensitive(NotSensitiveKeys, optional): Declared paths. Default:NotSensitiveKeys.NONE.
Returns: bool.
Return a deep copy of data with sensitive values replaced by REDACTED_PLACEHOLDER ("***REDACTED***").
Parameters:
data(dict[str, Any], required): Configuration mapping to redact.not_sensitive(Iterable[str] | None, optional): Dotted paths declared not sensitive; same validation asNotSensitiveKeys.of. Default:None.
Example:
from lib_layered_config import redact_mapping
print(redact_mapping({"max_token_bytes": 4096, "password": "x"}, not_sensitive=["max_token_bytes"]))
# {'max_token_bytes': 4096, 'password': '***REDACTED***'}Compute the canonical environment variable prefix for a slug.
Parameters:
slug(str, required): Configuration slug (e.g.,"config-kit").
Returns: Uppercase environment prefix with dashes converted to underscores (e.g., "CONFIG_KIT").
Examples:
Example 1: Generate documentation for environment variables
from lib_layered_config import default_env_prefix
# Calculate the prefix for your application
slug = "myapp"
prefix = default_env_prefix(slug)
print(f"Environment Variables for {slug}:")
print(f"=" * 50)
print(f"\n{prefix}<SECTION>__<KEY>=<value>\n")
print("Examples:")
print(f" {prefix}DATABASE__HOST=localhost")
print(f" {prefix}DATABASE__PORT=5432")
print(f" {prefix}SERVICE__TIMEOUT=30")
print(f"\nNote: Use double underscores (__) for nested keys")Explanation: Use this to generate documentation showing users how to set environment variables for your application.
Example 2: Programmatically set environment variables
import os
from lib_layered_config import default_env_prefix
# Calculate prefix
prefix = default_env_prefix("myapp")
# Set environment variables programmatically (useful in tests)
os.environ[f"{prefix}DATABASE__HOST"] = "test-db.example.com"
os.environ[f"{prefix}DATABASE__PORT"] = "5432"
os.environ[f"{prefix}SERVICE__TIMEOUT"] = "5"
# Now when you load configuration, these will be picked up
from lib_layered_config import read_config
config = read_config(vendor="Acme", app="MyApp", slug="myapp")
print(f"DB Host: {config.get('database.host')}") # test-db.example.comExplanation: Programmatically generate environment variable names for testing or dynamic configuration.
Example 3: Validate environment variable names
import os
from lib_layered_config import default_env_prefix
slug = "myapp"
expected_prefix = default_env_prefix(slug)
# Check if environment variables are correctly namespaced
print(f"Checking environment variables for prefix: {expected_prefix}_")
mismatched = []
for key in os.environ:
if "DATABASE" in key or "SERVICE" in key:
if not key.startswith(expected_prefix + "_"):
mismatched.append(key)
if mismatched:
print("\nWarning: Found environment variables that won't be loaded:")
for key in mismatched:
correct_name = f"{expected_prefix}_{key}"
print(f" {key} should be {correct_name}")
else:
print("All environment variables are correctly prefixed!")Explanation: Validate that your environment variables are correctly prefixed so they'll be picked up by the configuration loader.
Copy a source configuration file into one or more layer directories with conflict handling.
Parameters:
source(str | Path, required): Path to the configuration file to copy.vendor(str, required): Vendor namespace.app(str, required): Application name.targets(Sequence[str], required): Layer targets to deploy to. Valid values:"app","host","user".slug(str | None, optional): Configuration slug. Default:None(usesappas slug).profile(str | None, optional): Configuration profile name. Addsprofile/<name>/to deployment paths. Default:None.platform(str | None, optional): Override auto-detected platform. Valid values:"linux","darwin","windows", or any value starting with"win". Default:None(auto-detects from current platform).force(bool, optional): When True and file exists with different content, backup to.bakand overwrite. Default:False.batch(bool, optional): Non-interactive mode - keeps existing files and writes new config as.ucffor review (CI/CD). Default:False.conflict_resolver(Callable[[Path], DeployAction] | None, optional): Custom callback for conflict resolution. Default:None.set_permissions(bool | None, optional):Truesets modes,Falseleaves them to the umask,Nonefollows the configured[lib_layered_config.default_permissions].enabled(true when unset). An explicitdir_modeorfile_modemeans modes are set; one given withFalseis refused. Skipped on Windows. Default:None.dir_mode(int | None, optional): Directory mode for every target, overriding the configured and built-in ones. Default:None.file_mode(int | None, optional): File mode for every target, overriding the configured and built-in ones. Default:None.permissions(DeployPermissions | None, optional): A complete settings object to use instead of reading the configuration, for a caller that builds one on purpose. One built withdeploy_permissions_from_config(read_config(...))includes.env, the deployed files and a user file'sapp_*/host_*modes, which the deploy's own read leaves out; usepermission_overridesfor runtime overrides. Default:None.permission_overrides(Mapping[str, object] | None, optional): Runtime values for keys of[lib_layered_config.default_permissions], by setting name ({"user_file": "0o640"}), laid over the deploy's own read and validated like configured values; a refusal names(source: override). Validated on every call.{"enabled": False}withset_permissions=Noneand no mode turns permission setting off without reading the configuration. Cannot be combined withpermissions. Default:None.
Returns: list[DeployResult] - Each result contains:
destination: Path to the target fileaction:DeployActionenum (CREATED,OVERWRITTEN,KEPT,SKIPPED)backup_path: Path to.bakfile (if action wasOVERWRITTEN)ucf_path: Path to.ucffile (if action wasKEPT)dot_d_results: List ofDeployResultfor companion.ddirectory files (if any)
.d Directory Support: If the source file has a companion .d directory (e.g., defaults.toml → defaults.d/), those files are also deployed to the corresponding .d directory at each destination. User-added files in the destination .d directory are preserved.
Smart Skipping: If the source content is byte-identical to the existing destination file, the file is skipped without creating backups (regardless of force or batch flags). This applies to both base files and .d directory files.
Permissions: Each side (directory, file) resolves as dir_mode/file_mode, else the configured
[lib_layered_config.default_permissions] setting for that layer, else the built-in layer mode (app/host
755/644, user 700/600); an unsafe mode is refused before anything is written. A user-layer file decides only
user_* and enabled: its app_*/host_* values are ignored (logged, never refused). See
cli-reference.md#-file-permissions for the full resolution order and the
refusal rule.
Raises: FileNotFoundError (source file does not exist), PermissionError (the source, a file in its .d
directory, or a destination sits behind a permission wall; the same on every Python), ValueError (an invalid profile name),
ValidationError (an unknown target, before anything is written), DeployModeError (a refused dir_mode /
file_mode, or one given with set_permissions=False), DeployPermissionsError (the permission settings are
not readable, the section is invalid, a refused permission_overrides value, or permission_overrides
combined with permissions; nothing is written).
Examples:
Example 1: Deploy system-wide defaults
from lib_layered_config import deploy_config
from lib_layered_config.examples.deploy import DeployAction
# Deploy app-wide defaults to the system directory
results = deploy_config(
source="./config/defaults.toml",
vendor="Acme",
app="MyApp",
targets=["app"], # Deploy to system-wide location
slug="myapp",
)
# On Linux, this copies to: /etc/xdg/myapp/config.toml (+ config.d/ if exists)
# On macOS: /Library/Application Support/Acme/MyApp/config.toml (+ config.d/)
# On Windows: C:\ProgramData\Acme\MyApp\config.toml (+ config.d\)
for result in results:
print(f"{result.action.value}: {result.destination}")
for dot_d_result in result.dot_d_results:
print(f" .d: {dot_d_result.action.value}: {dot_d_result.destination}")Explanation: Use the "app" target to deploy system-wide defaults that all users share. If a defaults.d/ directory exists alongside defaults.toml, its contents are also deployed. This is typically done during installation.
Example 2: Deploy with batch mode (CI/CD safe)
from lib_layered_config import deploy_config
from lib_layered_config.examples.deploy import DeployAction
# Deploy in CI - keeps existing, writes new config as .ucf for review
results = deploy_config(
source="./my-config.toml",
vendor="Acme",
app="MyApp",
targets=["user"],
slug="myapp",
batch=True, # Non-interactive, creates .ucf for review
)
for result in results:
if result.action == DeployAction.CREATED:
print(f"Created: {result.destination}")
elif result.action == DeployAction.KEPT:
print(f"Kept: {result.destination}")
print(f" Review new config at: {result.ucf_path}")
elif result.action == DeployAction.SKIPPED:
print(f"Skipped (identical content): {result.destination}")Explanation: Use batch=True for CI/CD pipelines. When content differs, the existing file is kept and new config is written to .ucf for review.
Example 3: Deploy with force and check backups
from lib_layered_config import deploy_config
from lib_layered_config.examples.deploy import DeployAction
# Force deploy - creates backups before overwriting
results = deploy_config(
source="./new-config.toml",
vendor="Acme",
app="MyApp",
targets=["user"],
slug="myapp",
force=True, # Backup to .bak, then overwrite
)
for result in results:
if result.action == DeployAction.OVERWRITTEN:
print(f"Overwrote: {result.destination}")
print(f"Backup at: {result.backup_path}")
elif result.action == DeployAction.SKIPPED:
print(f"Skipped (content identical): {result.destination}")
elif result.action == DeployAction.CREATED:
print(f"Created: {result.destination}")Explanation: With force=True, existing files with different content are backed up to .bak before overwriting. If content is identical, files are smart-skipped without backups.
Example 4: Deploy to multiple layers at once
from lib_layered_config import deploy_config
from lib_layered_config.examples.deploy import DeployAction
# Deploy the same config to multiple layers
results = deploy_config(
source="./base-config.toml",
vendor="Acme",
app="MyApp",
targets=["app", "user"], # Deploy to both system and user directories
slug="myapp",
force=True,
)
print(f"Deployed to {len(results)} locations:")
for result in results:
status = "✓" if result.action in (DeployAction.CREATED, DeployAction.OVERWRITTEN) else "○"
print(f" {status} {result.destination} ({result.action.value})")Explanation: Deploy to multiple layers simultaneously. Useful for setting up consistent defaults across system and user levels. The force=True parameter allows overwriting existing files.
Example 5: Cross-platform deployment script
from lib_layered_config import deploy_config
from lib_layered_config.examples.deploy import DeployAction
import sys
# Deployment script that works across platforms
source_config = "./dist/config.toml"
print(f"Deploying configuration on {sys.platform}...")
try:
results = deploy_config(
source=source_config,
vendor="Acme",
app="MyApp",
targets=["app"],
slug="myapp",
batch=True, # Non-interactive for scripts
)
for result in results:
if result.action == DeployAction.CREATED:
print(f"✓ Created: {result.destination}")
elif result.action == DeployAction.KEPT:
print(f"○ Kept existing, review new config at: {result.ucf_path}")
elif result.action == DeployAction.SKIPPED:
print(f"○ Skipped (identical content): {result.destination}")
except FileNotFoundError:
print(f"Error: Source file '{source_config}' not found")
sys.exit(1)Explanation: The function automatically detects the platform and deploys to the appropriate directories. Use batch=True for non-interactive scripts; new configs are written to .ucf files for review.
Example 6: Deploy to a specific profile (environment-specific)
from lib_layered_config import deploy_config
from lib_layered_config.examples.deploy import DeployAction
# Deploy production configuration to the production profile
results = deploy_config(
source="./configs/production.toml",
vendor="Acme",
app="MyApp",
targets=["app"],
slug="myapp",
profile="production", # Deploy to profile-specific subdirectory
)
# On Linux: /etc/xdg/myapp/profile/production/config.toml (+ config.d/ if exists)
# On macOS: /Library/Application Support/Acme/MyApp/profile/production/config.toml
# On Windows: C:\ProgramData\Acme\MyApp\profile\production\config.toml
for result in results:
print(f"Production config: {result.action.value} -> {result.destination}")
# Deploy test configuration to a separate profile
test_results = deploy_config(
source="./configs/test.toml",
vendor="Acme",
app="MyApp",
targets=["app", "user"],
slug="myapp",
profile="test", # Completely isolated from production
)
# On Linux: /etc/xdg/myapp/profile/test/config.toml (+ config.d/)
# ~/.config/myapp/profile/test/config.toml (+ config.d/)Explanation: Use the profile parameter to deploy environment-specific configurations to isolated subdirectories. If the source has a companion .d directory, it's also deployed. This keeps production, staging, and test configurations completely separate, preventing accidental cross-environment configuration leaks.
Example 7: Deploy multiple profiles in a CI/CD pipeline
from lib_layered_config import deploy_config
from lib_layered_config.examples.deploy import DeployAction
from pathlib import Path
# Deploy configurations for all environments
environments = ["development", "staging", "production"]
for env in environments:
config_file = Path(f"./environments/{env}.toml")
if not config_file.exists():
print(f"Skipping {env}: config file not found")
continue
results = deploy_config(
source=config_file,
vendor="Acme",
app="MyApp",
targets=["app"],
slug="myapp",
profile=env,
force=True, # Update existing configs (creates backups)
)
for result in results:
if result.action == DeployAction.CREATED:
print(f"✓ {env}: created {result.destination}")
elif result.action == DeployAction.OVERWRITTEN:
print(f"✓ {env}: updated {result.destination} (backup: {result.backup_path})")
elif result.action == DeployAction.SKIPPED:
print(f"○ {env}: unchanged {result.destination}")Explanation: Profiles are ideal for CI/CD pipelines where you need to deploy different configurations for each environment. Each profile is isolated, so you can safely deploy all environments to the same system. With force=True, backups are created before overwriting.
Example 8: Deploy with custom permissions
from lib_layered_config import deploy_config
from lib_layered_config.examples.deploy import DeployAction
# Deploy with configured or built-in layer defaults (the default: set_permissions=None)
# - App layer: 755 dirs, 644 files (world-readable), unless configured otherwise
# - User layer: 700 dirs, 600 files (private), unless configured otherwise
results = deploy_config(
source="./config.toml",
vendor="Acme",
app="MyApp",
targets=["user"],
slug="myapp",
)
# Deploy with custom permissions (e.g., group-readable) - explicit modes win over configured ones
results = deploy_config(
source="./config.toml",
vendor="Acme",
app="MyApp",
targets=["app"],
slug="myapp",
dir_mode=0o750, # rwxr-x--- (owner + group read/execute)
file_mode=0o640, # rw-r----- (owner + group read)
)
# Deploy without setting permissions (use umask defaults)
results = deploy_config(
source="./config.toml",
vendor="Acme",
app="MyApp",
targets=["user"],
slug="myapp",
set_permissions=False, # Skip chmod, use system umask
)
# Runtime override of one setting (e.g. from an application's own --set option), laid over
# whatever deploy_config would otherwise read from the configured section
results = deploy_config(
source="./config.toml",
vendor="Acme",
app="MyApp",
targets=["user"],
slug="myapp",
permission_overrides={"user_file": "0o640"},
)
for result in results:
if result.action == DeployAction.CREATED:
print(f"Created: {result.destination}")
# Check actual permissions (Linux/macOS)
import stat
mode = result.destination.stat().st_mode
print(f" Mode: {stat.filemode(mode)}")Explanation: Leave set_permissions at its default (None) for configured-or-built-in, layer-aware
permissions. Use dir_mode and file_mode to override them for special requirements (e.g., group-readable
configs); an unsafe mode is refused. Use set_permissions=False when permissions should be inherited from
umask. Use permission_overrides for a runtime value (such as a --set option) laid over deploy's own read,
rather than building a permissions= object from read_config(...), which includes .env, the deployed
files and a user file's app_*/host_* modes. On Windows, permission setting is automatically skipped.
Generate example configuration trees for documentation or onboarding.
Parameters:
destination(str | Path, required): Directory that will receive the example tree.slug(str, required): Configuration slug used in generated files.vendor(str, required): Vendor namespace.app(str, required): Application name.force(bool, optional): Overwrite existing example files. Default:False.platform(str | None, optional): Override platform layout. Valid values:"posix","windows". Default:None(uses current platform).
Returns: List of Path objects for files created.
Examples:
Example 1: Generate documentation examples
from lib_layered_config import generate_examples
from pathlib import Path
# Generate example configuration files for documentation
docs_dir = Path("./docs/examples")
created_files = generate_examples(
destination=docs_dir,
slug="myapp",
vendor="Acme",
app="MyApp",
platform="posix", # Generate Linux/macOS examples
)
print(f"Generated {len(created_files)} example files:")
for file_path in created_files:
relative = file_path.relative_to(docs_dir)
print(f" - {relative}")
# Output shows:
# - etc/myapp/config.toml (app defaults)
# - etc/myapp/hosts/your-hostname.toml (host overrides)
# - xdg/myapp/config.toml (user preferences)
# - xdg/myapp/config.d/10-override.toml (split overrides)
# - .env.example (environment variables)Explanation: Perfect for generating example configurations to include in your documentation or repository. Users can copy these examples to get started quickly.
Example 2: Generate Windows examples for cross-platform project
from lib_layered_config import generate_examples
from pathlib import Path
# Generate Windows-specific examples even on Linux/macOS
windows_examples = Path("./docs/examples-windows")
created_files = generate_examples(
destination=windows_examples,
slug="myapp",
vendor="Acme",
app="MyApp",
platform="windows", # Force Windows layout
)
print("Windows configuration examples:")
for file_path in created_files:
print(f" {file_path.relative_to(windows_examples)}")
# Output shows Windows paths:
# - ProgramData/Acme/MyApp/config.toml
# - ProgramData/Acme/MyApp/config.d/10-override.toml
# - ProgramData/Acme/MyApp/hosts/your-hostname.toml
# - AppData/Roaming/Acme/MyApp/config.toml
# - AppData/Roaming/Acme/MyApp/config.d/10-override.toml
# - .env.exampleExplanation: Generate platform-specific examples regardless of your current OS. Great for maintaining documentation for all supported platforms.
Example 3: Onboarding script - Generate and customize examples
from lib_layered_config import generate_examples
from pathlib import Path
def onboard_user(username: str):
"""Generate personalized configuration examples for a new user."""
# Create user-specific examples directory
user_examples = Path(f"/tmp/{username}-config-examples")
user_examples.mkdir(exist_ok=True)
# Generate example files
created = generate_examples(destination=user_examples, slug="myapp", vendor="Acme", app="MyApp")
print(f"Generated {len(created)} example files for {username}:")
# Customize the examples with user-specific values
user_config = user_examples / "xdg/myapp/config.toml"
if user_config.exists():
content = user_config.read_text()
# Add user-specific comment
content = f"# Configuration for {username}\n" + content
user_config.write_text(content)
print(f"\nExamples generated in: {user_examples}")
print("Copy these files to get started:")
for f in created:
print(f" {f.relative_to(user_examples)}")
# Run onboarding
onboard_user("alice")Explanation: Generate examples as part of an onboarding workflow. You can then customize the generated files programmatically before presenting them to users.
Example 4: Update examples (force overwrite)
from lib_layered_config import generate_examples
from pathlib import Path
# Regenerate examples, overwriting existing ones
examples_dir = Path("./examples")
created = generate_examples(
destination=examples_dir,
slug="myapp",
vendor="Acme",
app="MyApp",
force=True, # Overwrite existing examples
)
print(f"Regenerated {len(created)} example files")
# This is useful when you update your configuration schema
# and need to refresh the documentation examplesExplanation: Use force=True when updating examples after schema changes. This ensures all example files reflect your latest configuration structure.
Example 5: Generate both POSIX and Windows examples
from lib_layered_config import generate_examples
from pathlib import Path
def generate_all_examples():
"""Generate examples for all platforms."""
base_dir = Path("./docs/config-examples")
# Generate POSIX examples
posix_files = generate_examples(
destination=base_dir / "linux-macos", slug="myapp", vendor="Acme", app="MyApp", platform="posix"
)
print(f"Generated {len(posix_files)} POSIX examples")
# Generate Windows examples
windows_files = generate_examples(
destination=base_dir / "windows", slug="myapp", vendor="Acme", app="MyApp", platform="windows"
)
print(f"Generated {len(windows_files)} Windows examples")
print(f"\nTotal: {len(posix_files) + len(windows_files)} example files")
print(f"Location: {base_dir}")
generate_all_examples()Explanation: Generate complete documentation showing users how to configure your app on any platform. This is essential for cross-platform applications.
Intentionally raise a RuntimeError for testing error handling.
Parameters: None
Raises: RuntimeError with message "i should fail".
Example:
from lib_layered_config import i_should_fail
try:
i_should_fail()
except RuntimeError as e:
print(f"Caught expected error: {e}")The library exports functions and constants for validating profile names before use. This is useful for pre-flight checks in configuration deployment or validating user input.
Constant defining the default maximum length for profile names.
Value: 64 (characters)
Example:
from lib_layered_config import DEFAULT_MAX_PROFILE_LENGTH
print(f"Profile names are limited to {DEFAULT_MAX_PROFILE_LENGTH} characters by default")
# Output: Profile names are limited to 64 characters by defaultValidate a profile name for safe use in filesystem paths.
Parameters:
value(str, required): The profile name to validate.max_length(int, optional): Maximum allowed length. Default:64characters. Set to0or negative to use the absolute maximum (256 chars).
Returns: The validated profile name (unchanged if valid).
Raises: ValueError when the profile name fails validation.
Security Checks:
- Length limit (default 64 chars, absolute max 256 chars for filesystem safety)
- No control characters (null bytes, newlines, tabs, etc.)
- No path traversal sequences (
../,..\\) - No path separators (
/,\\) - ASCII-only characters
- No Windows reserved names (CON, PRN, NUL, COM1-9, LPT1-9)
- Must start with alphanumeric character
- No trailing dots or spaces
Note: The max_length parameter is clamped to 256 characters for filesystem safety. Setting max_length=1000 will effectively use 256 as the limit.
Examples:
Example 1: Basic validation
from lib_layered_config import validate_profile_name
# Valid profile names
profile = validate_profile_name("production") # Returns "production"
profile = validate_profile_name("test-v2") # Returns "test-v2"
profile = validate_profile_name("dev_local") # Returns "dev_local"Example 2: Handling invalid input
from lib_layered_config import validate_profile_name
try:
validate_profile_name("") # Empty string
except ValueError as e:
print(f"Error: {e}") # "profile cannot be empty"
try:
validate_profile_name("../etc/passwd") # Path traversal
except ValueError as e:
print(f"Error: {e}") # "profile contains invalid characters: ../etc/passwd"
try:
validate_profile_name("a" * 100) # Too long
except ValueError as e:
print(f"Error: {e}") # "profile exceeds maximum length of 64: 100 characters"Example 3: Custom length limits
from lib_layered_config import validate_profile_name
# Allow longer profiles (up to 256 chars absolute max)
profile = validate_profile_name("a" * 100, max_length=200) # OK
# Even with max_length=1000, the absolute max of 256 applies
try:
validate_profile_name("a" * 257, max_length=1000)
except ValueError as e:
print(f"Error: {e}") # "profile exceeds maximum length of 256: 257 characters"Check if a profile name is valid without raising an exception.
Parameters:
value(str | None, required): The profile name to check.Noneis considered valid (no profile).max_length(int, optional): Maximum allowed length. Default:64characters. Set to0or negative to use the absolute maximum (256 chars).
Returns: True if the profile name is valid or None, False otherwise.
Note: The max_length parameter is clamped to 256 characters for filesystem safety, same as validate_profile_name.
Examples:
Example 1: Pre-flight validation
from lib_layered_config import is_valid_profile_name, read_config
def load_config_with_profile(profile: str | None):
"""Load configuration, validating the profile name first."""
if not is_valid_profile_name(profile):
print(f"Invalid profile name: {profile}")
return None
return read_config(vendor="Acme", app="MyApp", slug="myapp", profile=profile)
# Usage
config = load_config_with_profile("production") # Valid
config = load_config_with_profile("../etc") # Invalid, returns None
config = load_config_with_profile(None) # Valid (no profile)Example 2: User input validation
from lib_layered_config import is_valid_profile_name
def get_valid_profile():
"""Prompt user for a valid profile name."""
while True:
profile = input("Enter profile name (or 'none'): ").strip()
if profile.lower() == "none":
return None
if is_valid_profile_name(profile):
return profile
print("Invalid profile name. Use lowercase letters, numbers, hyphens, underscores.")
print("Cannot contain spaces, path separators, or special characters.")Example 3: Batch validation
from lib_layered_config import is_valid_profile_name
profiles = ["production", "test", "../hack", "staging-v2", "my profile"]
valid_profiles = [p for p in profiles if is_valid_profile_name(p)]
invalid_profiles = [p for p in profiles if not is_valid_profile_name(p)]
print(f"Valid: {valid_profiles}") # ['production', 'test', 'staging-v2']
print(f"Invalid: {invalid_profiles}") # ['../hack', 'my profile']The library exports constants for Unix file permissions used during deployment. These constants define sensible defaults for different configuration layers.
Directory permission mode for app/host layers (system-wide configs).
Value: 0o755 (rwxr-xr-x)
File permission mode for app/host layers (system-wide configs).
Value: 0o644 (rw-r--r--)
Directory permission mode for user layer (private configs).
Value: 0o700 (rwx------)
File permission mode for user layer (private configs).
Value: 0o600 (rw-------)
Example:
from lib_layered_config import (
DEFAULT_APP_DIR_MODE,
DEFAULT_APP_FILE_MODE,
DEFAULT_USER_DIR_MODE,
DEFAULT_USER_FILE_MODE,
)
print(f"App dir mode: {oct(DEFAULT_APP_DIR_MODE)}") # 0o755
print(f"App file mode: {oct(DEFAULT_APP_FILE_MODE)}") # 0o644
print(f"User dir mode: {oct(DEFAULT_USER_DIR_MODE)}") # 0o700
print(f"User file mode: {oct(DEFAULT_USER_FILE_MODE)}") # 0o600Explanation: App/host layers use world-readable permissions since system-wide configuration should be accessible by all processes. User layer uses private permissions since personal configuration should not be accessible by other users. On Windows, permissions are skipped (Windows uses ACLs instead).
Exported from the package root, used to configure or override deploy_config's permission handling.
DeployMode: An immutable, validated Unix permission mode; an unsafe one cannot be constructed.ModeKind: Whether a mode applies to a directory or a file (DIRECTORY,FILE).LayerModes: The directory mode and the file mode one layer is deployed with.DeployPermissions: Directory and file modes per deployment layer, plus whether to set them at all;.defaults()returns the built-in layer modes,.for_layer(layer)returns one layer'sLayerModes.deploy_permissions_from_config: Reads[lib_layered_config.default_permissions]from a merged configuration, with anoverrides=mapping laid over the configured values for building aDeployPermissionsyourself. It reads theConfigas it is, so a user file'sapp_*/host_*in aread_config(...)result decides there, unlike indeploy_config's own read. Fordeploy_configitself, pass runtime overrides through its ownpermission_overrides=parameter instead - see thedeploy_configsection above.DeployModeError: A permission setting was refused; the message names the reason or every offending bit.DeployPermissionsError: The permission settings cannot be used;.problemslists onePermissionProblemper bad value, and.hint(optional) suggests a way to deploy anyway.PermissionProblem: One refused setting: its dotted key, why, and (.source) where it was set, when known.