Mixins Deep Dive¶
Advanced Multi-File Operations¶
YAPFM provides powerful multi-file operations with various merge strategies for complex configuration management scenarios.
Merge Strategies Deep Dive¶
Deep Merge Strategy¶
The most commonly used strategy for configuration layering:
from yapfm import YAPFileManager
from yapfm.multi_file.strategies import MergeStrategy
fm = YAPFileManager("config.json")
# Deep merge with custom options
data = fm.load_multiple_files([
"base.json",
"environment.json",
"user.json"
], strategy="deep")
# Deep merge with conflict resolution
def resolve_conflicts(key, value1, value2):
"""Custom conflict resolution function."""
if key == "database.host":
return value2 # Always use the newer value
elif key == "logging.level":
# Use the more verbose level
levels = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "ERROR": 3}
return value1 if levels.get(value1, 0) > levels.get(value2, 0) else value2
else:
return value2 # Default: newer value wins
data = fm.load_multiple_files(
["base.json", "override.json"],
strategy="deep",
conflict_resolver=resolve_conflicts
)
Namespace Strategy with Custom Prefixes¶
Organize configurations by environment or component:
# Environment-based namespacing
data = fm.load_multiple_files([
"database.json",
"api.json",
"cache.json"
], strategy="namespace", namespace_prefix="services")
# Result structure:
# {
# "services": {
# "database": {...},
# "api": {...},
# "cache": {...}
# }
# }
# Component-based namespacing
data = fm.load_multiple_files([
"frontend.json",
"backend.json",
"mobile.json"
], strategy="namespace", namespace_prefix="components")
Priority Strategy with Complex Rules¶
Handle complex precedence scenarios:
# Define priority rules
priorities = {
"user.json": 100, # Highest priority
"environment.json": 50, # Medium priority
"base.json": 10 # Lowest priority
}
data = fm.load_multiple_files([
"base.json",
"environment.json",
"user.json"
], strategy="priority", priorities=priorities)
# Conditional priority based on file content
def dynamic_priority(file_path, data):
"""Calculate priority based on file content."""
if data.get("environment") == "production":
return 100
elif "override" in str(file_path):
return 75
else:
return 25
data = fm.load_multiple_files(
["base.json", "override.json", "prod.json"],
strategy="priority",
priority_calculator=dynamic_priority
)
Conditional Strategy for Dynamic Loading¶
Load files based on runtime conditions:
def load_environment_specific_files(environment):
"""Load files based on environment."""
def condition(file_path, data):
# Load base files always
if "base" in str(file_path):
return True
# Load environment-specific files
if environment in str(file_path):
return True
# Load files that match current environment
if data.get("environment") == environment:
return True
return False
return fm.load_multiple_files(
["base.json", "dev.json", "prod.json", "staging.json"],
strategy="conditional",
condition_func=condition
)
# Load production configuration
prod_config = load_environment_specific_files("production")
Advanced File Group Management¶
Organize complex multi-file scenarios:
# Define comprehensive file groups
file_groups = {
"core_services": {
"files": ["database.json", "cache.json", "queue.json"],
"strategy": "namespace",
"namespace_prefix": "core"
},
"api_services": {
"files": ["rest.json", "graphql.json", "websocket.json"],
"strategy": "namespace",
"namespace_prefix": "api"
},
"environment_config": {
"files": ["base.json", "dev.json", "prod.json"],
"strategy": "priority",
"priorities": {"prod.json": 100, "dev.json": 50, "base.json": 10}
},
"user_preferences": {
"files": ["defaults.json", "user.json"],
"strategy": "deep",
"conditional_filter": lambda path, data: "user" in str(path)
}
}
# Load specific groups
core_config = fm.load_file_group("core_services", file_groups)
api_config = fm.load_file_group("api_services", file_groups)
# Load all groups into a single configuration
all_config = {}
for group_name in file_groups:
group_config = fm.load_file_group(group_name, file_groups)
all_config.update(group_config)
Performance Optimization with Caching¶
Optimize multi-file operations with intelligent caching:
# Enable caching for multi-file operations
fm = YAPFileManager("config.json", enable_cache=True, cache_size=5000)
# Load with caching enabled
data = fm.load_multiple_files([
"base.json",
"environment.json",
"user.json"
], strategy="deep", use_cache=True)
# Cache statistics for multi-file operations
stats = fm.get_cache_stats()
print(f"Multi-file cache hits: {stats['multi_file']['hits']}")
print(f"Cache efficiency: {stats['multi_file']['hit_rate']:.2%}")
# Invalidate specific file caches
fm.invalidate_multi_file_cache("environment.json")
Error Handling and Validation¶
Robust error handling for multi-file operations:
def safe_load_multiple_files(fm, files, strategy="deep", **kwargs):
"""Safely load multiple files with error handling."""
try:
return fm.load_multiple_files(files, strategy=strategy, **kwargs)
except FileNotFoundError as e:
print(f"File not found: {e}")
# Try loading available files
available_files = [f for f in files if Path(f).exists()]
if available_files:
return fm.load_multiple_files(available_files, strategy=strategy, **kwargs)
return {}
except Exception as e:
print(f"Error loading files: {e}")
return {}
# Usage with error handling
data = safe_load_multiple_files(
fm,
["base.json", "missing.json", "environment.json"],
strategy="deep"
)
Unified API and Batch Operations¶
YAPFM provides a unified API that simplifies common operations while maintaining the power of individual mixins.
Unified API Benefits¶
The unified API provides:
- Simplified syntax: fm.set() instead of fm.set_key()
- Dictionary-like interface: fm["key"] for natural Python syntax
- Consistent behavior: All operations work the same way across formats
- Performance optimization: Batch operations for efficiency
Batch Operations Deep Dive¶
Efficient Multi-Key Operations¶
from yapfm import YAPFileManager
fm = YAPFileManager("config.json")
# Traditional approach (inefficient)
fm.set("database.host", "localhost")
fm.set("database.port", 5432)
fm.set("database.ssl", True)
fm.set("api.version", "v2")
fm.set("api.timeout", 30)
# Batch approach (efficient)
fm.set_multiple({
"database.host": "localhost",
"database.port": 5432,
"database.ssl": True,
"api.version": "v2",
"api.timeout": 30
})
# Batch operations with error handling
try:
fm.set_multiple({
"valid.key": "value",
"invalid.key": None # This might fail
})
except ValueError as e:
print(f"Some keys failed to set: {e}")
Advanced Batch Retrieval¶
# Get multiple values with different defaults
values = fm.get_multiple([
"database.host",
"database.port",
"api.timeout",
"logging.level"
], defaults={
"database.host": "localhost",
"database.port": 5432,
"api.timeout": 30,
"logging.level": "INFO"
})
# Check existence of multiple keys
exists = fm.has_multiple([
"database.host",
"database.port",
"missing.key"
])
# Returns: {"database.host": True, "database.port": True, "missing.key": False}
# Delete multiple keys with validation
deleted_count = fm.delete_multiple([
"temp.key1",
"temp.key2",
"temp.key3"
])
print(f"Deleted {deleted_count} keys")
Performance Optimization with Batch Operations¶
import time
from yapfm import YAPFileManager
fm = YAPFileManager("large_config.json", enable_cache=True)
# Measure performance difference
def measure_performance():
# Individual operations
start = time.time()
for i in range(1000):
fm.set(f"key{i}", f"value{i}")
individual_time = time.time() - start
# Batch operations
start = time.time()
batch_data = {f"batch_key{i}": f"value{i}" for i in range(1000)}
fm.set_multiple(batch_data)
batch_time = time.time() - start
print(f"Individual operations: {individual_time:.3f}s")
print(f"Batch operations: {batch_time:.3f}s")
print(f"Speedup: {individual_time/batch_time:.1f}x")
measure_performance()
Dictionary-like Interface¶
YAPFM supports full dictionary-like syntax for seamless integration:
# Natural Python syntax
fm["database.host"] = "localhost"
host = fm["database.host"]
"database.host" in fm
del fm["database.host"]
# Iteration support
for key in fm:
print(f"Key: {key}")
for key, value in fm.items():
print(f"{key}: {value}")
# Dictionary methods
fm.update({
"new.key1": "value1",
"new.key2": "value2"
})
# Pop with default
value = fm.pop("key", "default_value")
# Clear all data
fm.clear()
Advanced Caching with Unified API¶
# Enable advanced caching
fm = YAPFileManager(
"config.json",
enable_cache=True,
cache_size=2000,
cache_ttl=7200 # 2 hours
)
# Cache-aware operations
fm.set("database.host", "localhost") # Automatically cached
host = fm.get("database.host") # Retrieved from cache
# Batch operations with cache optimization
fm.set_multiple({
"key1": "value1",
"key2": "value2",
"key3": "value3"
}) # All keys cached efficiently
# Cache statistics
stats = fm.get_cache_stats()
print(f"Cache hit rate: {stats['unified_cache']['hit_rate']:.2%}")
print(f"Cache size: {stats['unified_cache']['size']}")
print(f"Memory usage: {stats['unified_cache']['memory_usage']} bytes")
# Cache invalidation
fm.invalidate_cache("database.*") # Invalidate all database keys
fm.clear_cache() # Clear all cache
Error Handling and Validation¶
def safe_batch_operations(fm, operations):
"""Safely perform batch operations with error handling."""
results = {
"successful": [],
"failed": [],
"skipped": []
}
for operation_type, data in operations.items():
try:
if operation_type == "set":
fm.set_multiple(data)
results["successful"].extend(data.keys())
elif operation_type == "delete":
deleted = fm.delete_multiple(data)
results["successful"].extend(data[:deleted])
results["skipped"].extend(data[deleted:])
elif operation_type == "get":
values = fm.get_multiple(data)
results["successful"].extend(values.keys())
except Exception as e:
results["failed"].append({
"operation": operation_type,
"data": data,
"error": str(e)
})
return results
# Usage
operations = {
"set": {"key1": "value1", "key2": "value2"},
"delete": ["old_key1", "old_key2"],
"get": ["key1", "key2", "key3"]
}
results = safe_batch_operations(fm, operations)
print(f"Successful: {len(results['successful'])}")
print(f"Failed: {len(results['failed'])}")
print(f"Skipped: {len(results['skipped'])}")
Creating Custom Mixins¶
from yapfm.mixins import FileOperationsMixin
from typing import Any, Dict, List, Optional
import hashlib
class ValidationMixin:
"""Mixin for configuration validation."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._validation_rules: Dict[str, Any] = {}
self._validation_errors: List[str] = []
def add_validation_rule(self, key: str, rule: callable, message: str = None) -> None:
"""Add a validation rule for a configuration key."""
self._validation_rules[key] = {
"rule": rule,
"message": message or f"Validation failed for key: {key}"
}
def validate_key(self, key: str, value: Any) -> bool:
"""Validate a single key."""
if key not in self._validation_rules:
return True
rule = self._validation_rules[key]["rule"]
try:
result = rule(value)
if not result:
self._validation_errors.append(self._validation_rules[key]["message"])
return result
except Exception as e:
self._validation_errors.append(f"Validation error for {key}: {e}")
return False
def validate_all(self) -> bool:
"""Validate all configuration keys."""
self._validation_errors.clear()
if not self.is_loaded():
self.load()
# Validate all keys in the document
self._validate_dict(self.data, "")
return len(self._validation_errors) == 0
def _validate_dict(self, data: Dict[str, Any], prefix: str) -> None:
"""Recursively validate dictionary data."""
for key, value in data.items():
full_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, dict):
self._validate_dict(value, full_key)
else:
self.validate_key(full_key, value)
def get_validation_errors(self) -> List[str]:
"""Get validation errors."""
return self._validation_errors.copy()
def set_key_with_validation(self, value: Any, dot_key: str) -> bool:
"""Set a key with validation."""
if self.validate_key(dot_key, value):
self.set_key(value, dot_key=dot_key)
return True
return False
class EncryptionMixin:
"""Mixin for configuration encryption."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._encryption_key: Optional[bytes] = None
def set_encryption_key(self, key: str) -> None:
"""Set encryption key."""
self._encryption_key = key.encode('utf-8')
def encrypt_value(self, value: str) -> str:
"""Encrypt a string value."""
if not self._encryption_key:
return value
from cryptography.fernet import Fernet
f = Fernet(self._encryption_key)
return f.encrypt(value.encode('utf-8')).decode('utf-8')
def decrypt_value(self, encrypted_value: str) -> str:
"""Decrypt a string value."""
if not self._encryption_key:
return encrypted_value
from cryptography.fernet import Fernet
f = Fernet(self._encryption_key)
return f.decrypt(encrypted_value.encode('utf-8')).decode('utf-8')
def set_encrypted_key(self, value: str, dot_key: str) -> None:
"""Set an encrypted configuration key."""
encrypted_value = self.encrypt_value(value)
self.set_key(encrypted_value, dot_key=dot_key)
def get_encrypted_key(self, dot_key: str, default: str = None) -> str:
"""Get and decrypt a configuration key."""
encrypted_value = self.get_key(dot_key=dot_key, default=default)
if encrypted_value is None:
return default
return self.decrypt_value(encrypted_value)
# Create a custom file manager with mixins
class AdvancedFileManager(
FileOperationsMixin,
ValidationMixin,
EncryptionMixin
):
def __init__(self, path, **kwargs):
self.path = path
super().__init__(**kwargs)
Using Custom Mixins¶
# Create advanced file manager
fm = AdvancedFileManager("secure_config.json", auto_create=True)
# Set up validation rules
fm.add_validation_rule("database.port", lambda x: isinstance(x, int) and 1 <= x <= 65535)
fm.add_validation_rule("app.version", lambda x: isinstance(x, str) and len(x) > 0)
# Set up encryption
fm.set_encryption_key("my-secret-key")
# Use validation
fm.set_key_with_validation(5432, dot_key="database.port") # Valid
fm.set_key_with_validation("invalid", dot_key="database.port") # Invalid
# Use encryption
fm.set_encrypted_key("secret-password", dot_key="database.password")
password = fm.get_encrypted_key(dot_key="database.password")