Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 88 additions & 13 deletions conftier/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
TypeVar,
Union,
cast,
get_args,
get_origin,
)

import yaml
Expand Down Expand Up @@ -66,7 +68,10 @@ def __init__(self, schema_type: SchemaType, model_instance: Any):

@classmethod
def from_schema(
cls, schema: Type[Any], data: Optional[Dict[str, Any]] = None
cls,
schema: Type[Any],
data: Optional[Dict[str, Any]] = None,
strict: bool = False,
) -> "ConfigModel":
"""
Create a ConfigModel from a schema type and data
Expand Down Expand Up @@ -102,7 +107,7 @@ def from_schema(
schema_type = SCHEMA_TYPE_DATACLASS
if data:
# Handle nested dataclasses
kwargs = cls._prepare_dataclass_kwargs(schema, data)
kwargs = cls._prepare_dataclass_kwargs(schema, data, strict=strict)
instance = schema(**kwargs)
else:
instance = schema()
Expand All @@ -116,46 +121,113 @@ def from_schema(

@staticmethod
def _prepare_dataclass_kwargs(
dataclass_type: Type[Any], data: Dict[str, Any]
dataclass_type: Type[Any],
data: Dict[str, Any],
*,
strict: bool = False,
validate_types: bool = True,
) -> Dict[str, Any]:
"""Prepare kwargs for dataclass initialization with proper handling of nested
dataclasses.

"""Improved dataclass kwargs preparation method
Args:
dataclass_type: Target dataclass type
data: Data dictionary
data: Input data dictionary
strict: Whether to enable strict mode (forbids extra fields)
validate_types: Whether to validate types

Returns:
Dictionary of kwargs suitable for initializing the dataclass
kwargs dictionary suitable for initializing the dataclass

Raises:
ValueError: Extra fields found in strict mode
TypeError: Type validation failed
"""
if not data:
return {}

if strict:
field_names = {f.name for f in fields(dataclass_type)}
extra_fields = set(data.keys()) - field_names
if extra_fields:
raise ValueError(f"Unexpected fields: {extra_fields}")

kwargs: Dict[str, Any] = {}

# Process each field in the dataclass
for field in fields(dataclass_type):
field_name = field.name

# Skip if field not in data
# Handle default values
if field_name not in data:
if field.default is not MISSING:
kwargs[field_name] = field.default
elif field.default_factory is not MISSING:
kwargs[field_name] = field.default_factory()
else:
continue
continue

field_value = data[field_name]
field_type = field.type

# Type validation
if validate_types and not ConfigModel._validate_type(
field_type, field_value
):
raise TypeError(
f"Field '{field_name}' expects type {field_type}, "
f"got {type(field_value)} with value {field_value}"
)

# Handle nested dataclasses
if isinstance(field_value, dict) and is_dataclass(field_type):
# Recursively handle nested dataclass
nested_kwargs = ConfigModel._prepare_dataclass_kwargs(
field_type, field_value
field_type,
field_value,
strict=strict,
validate_types=validate_types,
)
kwargs[field_name] = field_type(**nested_kwargs)
else:
kwargs[field_name] = field_value

return kwargs

@staticmethod
def _validate_type(field_type, value):
# Handle Optional type
if get_origin(field_type) is Union:
args = get_args(field_type)
if type(None) in args: # Is Optional type
if value is None:
return True
# Check non-None part
non_none_types = [t for t in args if t is not type(None)]
return any(ConfigModel._validate_type(t, value) for t in non_none_types)

# Handle container types like List, Dict
origin = get_origin(field_type)
if origin:
if origin is list:
return isinstance(value, list) and all(
ConfigModel._validate_type(get_args(field_type)[0], item)
for item in value
)
elif origin is dict:
return isinstance(value, dict) and all(
ConfigModel._validate_type(get_args(field_type)[0], k)
and ConfigModel._validate_type(get_args(field_type)[1], v)
for k, v in value.items()
)

# Basic type checking
if inspect.isclass(field_type) and not isinstance(value, field_type):
try:
# Try type conversion
return field_type(value)
except (TypeError, ValueError):
return False

return True

@property
def model(self) -> Any:
"""Get the underlying model instance"""
Expand Down Expand Up @@ -275,6 +347,7 @@ def __init__(
version: str = "1.0.0",
auto_create_user: bool = False,
auto_create_project: bool = False,
strict: bool = False,
):
"""
Initialize the configuration manager
Expand All @@ -286,6 +359,7 @@ def __init__(
version: Configuration schema version
auto_create_user: Whether to automatically create user config file if not
exists auto_create_project: Whether to automatically create project config
strict: Turn on strict extra field verification mode
file if not exists.
"""
self.config_name: str = config_name
Expand All @@ -294,6 +368,7 @@ def __init__(
self.auto_create_user: bool = auto_create_user
self.auto_create_project: bool = auto_create_project
self.schema_type: SchemaType
self.strict: bool = strict

if (
PYDANTIC_AVAILABLE
Expand Down Expand Up @@ -464,7 +539,7 @@ def _load_config_from_path(
return None, None

config_model: ConfigModel = ConfigModel.from_schema(
self.config_schema, config_dict
self.config_schema, config_dict, strict=self.strict
)
typed_config: T = cast(T, config_model.model)
return config_model, typed_config
Expand Down
50 changes: 30 additions & 20 deletions docs/guide/application-journey.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@ sequenceDiagram
participant Dev as App Developer
participant Conftier as Conftier Framework
participant App as Application

Note over Dev: 1. Configuration Definition
Dev->>Conftier: Define configuration schema<br/>(Pydantic/Dataclass)
Note right of Dev: AppConfig with API settings,<br/>database configs, feature flags

Note over Dev: 2. Integration Setup
Dev->>Conftier: Initialize ConfigManager<br/>(name="myapp", schema=AppConfig)
Conftier-->>Dev: Configuration manager ready

Note over Dev: 3. Configuration Usage
Dev->>Conftier: Load configuration
Conftier-->>Dev: Return configuration (from project or defaults)
Dev->>App: Initialize app with configuration

Note over Dev: 4. Environment Management
Dev->>Conftier: Set up different project configs<br/>for different environments
Note right of Dev: Create templates or CI scripts for<br/>dev/staging/production
Expand Down Expand Up @@ -84,7 +84,7 @@ class AIModelConfig(BaseModel):
temperature: float = Field(default=0.7, description="Model temperature")
api_key: str = Field(default="", description="API key")
max_tokens: int = Field(default=2000, description="Maximum number of tokens")

class LoggingConfig(BaseModel):
level: str = Field(default="INFO", description="Logging level")
file: Optional[str] = Field(default=None, description="Log file path")
Expand Down Expand Up @@ -117,7 +117,7 @@ ai_model:
temperature: 0.7
api_key: ""
max_tokens: 2000

logging:
level: INFO
file: null
Expand All @@ -139,7 +139,8 @@ config_manager = ConfigManager(
config_name="myapp",
config_schema=AppConfig,
version="1.0.0",
auto_create_project=True # Automatically create project config if it doesn't exist
auto_create_project=True, # Automatically create project config if it doesn't exist
strict=False, # Whether to enable strict mode to forbid undefined fields in config files
)
```

Expand All @@ -148,6 +149,12 @@ When `auto_create_project=True`:
- If the project-level config file doesn't exist at `./.myapp/config.yaml`, it will be created with default values
- This ensures your application always has a configuration file to work with

When `strict=True`:

- Configuration fields must strictly match the schema definition
- If the config file contains fields not defined in the schema, a ValueError will be raised
- This helps ensure configuration file standardization and prevents typos

### 3. Loading and Using Configuration

Now let's set up the main application initialization that loads and uses the configuration:
Expand All @@ -163,32 +170,32 @@ from myapp.ai import setup_ai_model
def create_app():
# Load the configuration (or use config_manager.config)
config = config_manager.load()

# Set up logging first
setup_logging(config.logging)

# Create the FastAPI app with configuration
app = FastAPI(
title="MyApp",
description="FastAPI Application with AI Features",
debug=config.server.debug
)

# Set up database
db = setup_database(config.database)

# Set up AI model client
ai_client = setup_ai_model(config.ai_model)

# Attach configuration and components to app for use in route handlers
app.state.config = config
app.state.db = db
app.state.ai_client = ai_client

# Enable features based on configuration
for feature in config.enable_features:
enable_feature(app, feature)

return app

# In main.py (app entrypoint)
Expand All @@ -202,7 +209,7 @@ if __name__ == "__main__":
# Get configuration for the server
config = config_manager.load()
server_config = config.server

# Run the server with the configured settings
uvicorn.run(
"main:app",
Expand All @@ -224,7 +231,6 @@ server:
database:
url: sqlite:///./dev.db
echo: true

# Production environment example (./.myapp/config.yaml)
# server:
# host: 0.0.0.0
Expand Down Expand Up @@ -272,17 +278,17 @@ config_manager = ConfigManager(
def load_config_with_env():
"""Load configuration with environment variable overrides."""
config = config_manager.load()

# Override with environment variables if they exist
if os.environ.get("MYAPP_AI_API_KEY"):
config.ai_model.api_key = os.environ.get("MYAPP_AI_API_KEY")

if os.environ.get("MYAPP_DB_URL"):
config.database.url = os.environ.get("MYAPP_DB_URL")

if os.environ.get("MYAPP_LOG_LEVEL"):
config.logging.level = os.environ.get("MYAPP_LOG_LEVEL")

return config
```

Expand Down Expand Up @@ -347,18 +353,22 @@ myapp config set --key server.port --value 5000
## Best Practices for Application Configuration

1. **Keep secrets out of configuration files**

- Use environment variables for sensitive values like API keys and credentials
- Provide placeholders in configuration templates

2. **Version control considerations**

- Add `./.myapp/config.yaml` to your `.gitignore` file
- Include a `config.example.yaml` file in version control as a template

3. **Documentation**

- Document all configuration options
- Provide example configurations for different environments

4. **Validation**

- Use Pydantic's validation capabilities to catch configuration errors early
- Add custom validation hooks for complex rules

Expand Down
Loading