configplusplus 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ """
2
+ ConfigPlusPlus - Beautiful configuration management for Python
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+ __author__ = "Florian BARRE"
7
+
8
+ from configplusplus.base import ConfigBase, ConfigMeta
9
+ from configplusplus.env_loader import EnvConfigLoader
10
+ from configplusplus.yaml_loader import YamlConfigLoader
11
+ from configplusplus.utils import env, safe_load_envs
12
+
13
+ __all__ = [
14
+ "ConfigBase",
15
+ "ConfigMeta",
16
+ "EnvConfigLoader",
17
+ "YamlConfigLoader",
18
+ "env",
19
+ "safe_load_envs",
20
+ ]
configplusplus/base.py ADDED
@@ -0,0 +1,138 @@
1
+ """
2
+ Base classes for configuration management with beautiful display
3
+ """
4
+
5
+ from typing import Any, Dict
6
+ import pathlib
7
+
8
+
9
+ class ConfigMeta(type):
10
+ """
11
+ Metaclass to provide pretty printing and helpers on configuration classes.
12
+
13
+ Automatically adds:
14
+ - to_dict(): Convert config to dictionary
15
+ - Pretty __repr__ with grouped display
16
+ - Secret masking for sensitive values
17
+ """
18
+
19
+ def to_dict(cls) -> Dict[str, Any]:
20
+ """
21
+ Return all UPPERCASE, non-callable attributes as a dict.
22
+
23
+ Returns:
24
+ Dictionary containing all configuration values
25
+ """
26
+ return {
27
+ k: v for k, v in cls.__dict__.items() if k.isupper() and not callable(v)
28
+ }
29
+
30
+ def _mask_if_secret(cls, key: str, value: Any) -> Any:
31
+ """
32
+ Mask potentially sensitive values (API keys, tokens, secrets, passwords).
33
+
34
+ Args:
35
+ key: Configuration key name
36
+ value: Configuration value
37
+
38
+ Returns:
39
+ Masked value if sensitive, original value otherwise
40
+ """
41
+ if value is None:
42
+ return None
43
+
44
+ key_upper = key.upper()
45
+ sensitive_keywords = ("SECRET", "API_KEY", "PASSWORD", "TOKEN", "CREDENTIAL")
46
+
47
+ if any(keyword in key_upper for keyword in sensitive_keywords):
48
+ s = str(value)
49
+ if len(s) <= 6:
50
+ return "***hidden***"
51
+ return f"{s[:3]}…{s[-2:]} (hidden)"
52
+
53
+ return value
54
+
55
+ def _grouped_items(cls) -> Dict[str, list]:
56
+ """
57
+ Group configuration items by prefix before first underscore.
58
+
59
+ Example:
60
+ QDRANT_URL and QDRANT_PORT -> grouped under "QDRANT"
61
+
62
+ Returns:
63
+ Dictionary mapping prefixes to list of (key, value) tuples
64
+ """
65
+ items = cls.to_dict()
66
+ groups: Dict[str, list] = {}
67
+
68
+ for k, v in items.items():
69
+ prefix = k.split("_", 1)[0] # e.g., QDRANT_URL -> QDRANT
70
+ groups.setdefault(prefix, []).append((k, v))
71
+
72
+ return groups
73
+
74
+ def __repr__(cls) -> str:
75
+ """
76
+ Pretty multi-line representation of the configuration.
77
+
78
+ Returns:
79
+ Formatted string with grouped configuration display
80
+ """
81
+ lines = ["\n"]
82
+ lines.append("╔════════════════════════════════════════════╗")
83
+ lines.append(f"║ {cls.__name__.upper().center(40)} ║")
84
+ lines.append("╚════════════════════════════════════════════╝")
85
+
86
+ groups = cls._grouped_items()
87
+
88
+ # Sort groups by name for deterministic output
89
+ for prefix in sorted(groups.keys()):
90
+ lines.append("") # blank line
91
+ lines.append(f"▶ {prefix}")
92
+ items = groups[prefix]
93
+
94
+ if not items:
95
+ continue
96
+
97
+ max_key_len = max(len(k) for k, _ in items)
98
+
99
+ for key, value in sorted(items, key=lambda kv: kv[0]):
100
+ display_value = cls._mask_if_secret(key, value)
101
+
102
+ # Make paths nicer to read
103
+ if isinstance(display_value, pathlib.Path):
104
+ display_value = str(display_value.resolve())
105
+
106
+ lines.append(f" {key.ljust(max_key_len)} = {display_value!r}")
107
+
108
+ lines.append("") # final blank line
109
+ return "\n".join(lines)
110
+
111
+
112
+ class ConfigBase(metaclass=ConfigMeta):
113
+ """
114
+ Base class for all configuration classes.
115
+
116
+ Provides:
117
+ - Pretty printing via metaclass
118
+ - to_dict() method for serialization
119
+ - Automatic grouping and display of config values
120
+
121
+ Usage:
122
+ class MyConfig(ConfigBase):
123
+ DATABASE_HOST = "localhost"
124
+ DATABASE_PORT = 5432
125
+ SECRET_API_KEY = "secret123"
126
+
127
+ print(MyConfig) # Pretty formatted output
128
+ """
129
+
130
+ def __repr__(self) -> str:
131
+ """Instance-level repr uses the class pretty repr."""
132
+ # Call the metaclass __repr__ directly
133
+ return ConfigMeta.__repr__(type(self))
134
+
135
+ def __str__(self) -> str:
136
+ """Instance-level str uses the class pretty repr."""
137
+ # Call the metaclass __repr__ directly
138
+ return ConfigMeta.__repr__(type(self))
@@ -0,0 +1,134 @@
1
+ """
2
+ Environment variable based configuration loader
3
+ """
4
+
5
+ from typing import Any
6
+ from configplusplus.base import ConfigBase
7
+
8
+
9
+ class EnvConfigLoader(ConfigBase):
10
+ """
11
+ Base class for environment variable based configuration.
12
+
13
+ This class is 100% static with no __init__ - configuration is loaded
14
+ from environment variables at class definition time.
15
+
16
+ Features:
17
+ - Automatic pretty printing via ConfigBase
18
+ - Secret masking for sensitive values
19
+ - Grouped display by configuration prefix
20
+
21
+ Usage:
22
+ from configplusplus import EnvConfigLoader, env
23
+
24
+ class MyConfig(EnvConfigLoader):
25
+ # Required variables
26
+ DATABASE_HOST = env("DATABASE_HOST")
27
+ DATABASE_PORT = env("DATABASE_PORT", cast=int)
28
+
29
+ # Optional with defaults
30
+ DEBUG_MODE = env("DEBUG_MODE", cast=bool, default=False)
31
+
32
+ # Paths
33
+ DATA_DIR = env("DATA_DIR", cast=pathlib.Path)
34
+
35
+ # Secrets (automatically masked in output)
36
+ SECRET_API_KEY = env("SECRET_API_KEY")
37
+
38
+ # Use as static class
39
+ print(MyConfig.DATABASE_HOST)
40
+ print(MyConfig) # Pretty formatted output
41
+
42
+ Helper Methods:
43
+ You can use the env() helper function with various options:
44
+
45
+ env(key) # Required, str
46
+ env(key, cast=int) # Required, int
47
+ env(key, default="value") # Optional with default
48
+ env(key, cast=bool, default=False) # Bool casting
49
+ env(key, cast=pathlib.Path) # Path casting
50
+ env_optional(key, default=None) # Explicitly optional
51
+
52
+ Boolean Casting:
53
+ When cast=bool, these strings are considered False:
54
+ - "false", "False", "FALSE"
55
+ - "0"
56
+ - "no", "No", "NO"
57
+ - "" (empty string)
58
+
59
+ All other values are considered True.
60
+
61
+ Secret Masking:
62
+ Variables containing these keywords are automatically masked:
63
+ - SECRET
64
+ - API_KEY
65
+ - PASSWORD
66
+ - TOKEN
67
+ - CREDENTIAL
68
+
69
+ Example output: "sec...et (hidden)"
70
+ """
71
+
72
+ @classmethod
73
+ def get(cls, key: str, default: Any = None) -> Any:
74
+ """
75
+ Get a configuration value by key.
76
+
77
+ Args:
78
+ key: Configuration key (case-insensitive)
79
+ default: Default value if key not found
80
+
81
+ Returns:
82
+ Configuration value or default
83
+
84
+ Example:
85
+ >>> MyConfig.get("DATABASE_HOST")
86
+ "localhost"
87
+
88
+ >>> MyConfig.get("MISSING_KEY", default="fallback")
89
+ "fallback"
90
+ """
91
+ return getattr(cls, key.upper(), default)
92
+
93
+ @classmethod
94
+ def has(cls, key: str) -> bool:
95
+ """
96
+ Check if a configuration key exists.
97
+
98
+ Args:
99
+ key: Configuration key (case-insensitive)
100
+
101
+ Returns:
102
+ True if key exists, False otherwise
103
+
104
+ Example:
105
+ >>> MyConfig.has("DATABASE_HOST")
106
+ True
107
+
108
+ >>> MyConfig.has("MISSING_KEY")
109
+ False
110
+ """
111
+ return hasattr(cls, key.upper())
112
+
113
+ @classmethod
114
+ def validate(cls) -> None:
115
+ """
116
+ Validate that all required configuration is present.
117
+
118
+ Override this method in subclasses to add custom validation logic.
119
+
120
+ Raises:
121
+ RuntimeError: If validation fails
122
+
123
+ Example:
124
+ class MyConfig(EnvConfigLoader):
125
+ DATABASE_HOST = env("DATABASE_HOST")
126
+ DATABASE_PORT = env("DATABASE_PORT", cast=int)
127
+
128
+ @classmethod
129
+ def validate(cls) -> None:
130
+ super().validate()
131
+ if cls.DATABASE_PORT < 1024:
132
+ raise RuntimeError("DATABASE_PORT must be >= 1024")
133
+ """
134
+ pass
@@ -0,0 +1,118 @@
1
+ """
2
+ Utility functions for configuration management
3
+ """
4
+
5
+ from typing import Any, TypeVar
6
+ from dotenv import load_dotenv
7
+ from loggerplusplus import loggerplusplus
8
+ from loggerplusplus import formats as lpp_formats
9
+ import sys
10
+ import os
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ def safe_load_envs(env_path: str = ".env", verbose: bool = True) -> bool:
16
+ """
17
+ Load environment variables from .env file with detailed logging.
18
+
19
+ Args:
20
+ env_path: Path to the .env file (default: ".env")
21
+ verbose: Whether to log loading information (default: True)
22
+
23
+ Returns:
24
+ True if file was loaded successfully, False otherwise
25
+
26
+ Example:
27
+ >>> safe_load_envs()
28
+ ✅ Loaded environment file: .env
29
+ True
30
+ """
31
+ if verbose:
32
+ loggerplusplus.add(
33
+ sink=sys.stdout,
34
+ level="DEBUG",
35
+ format=lpp_formats.ShortFormat(),
36
+ )
37
+ env_logger = loggerplusplus.bind(identifier="ENV_LOADER")
38
+
39
+ # Try with leading slash first (absolute path)
40
+ success = load_dotenv(f"/{env_path}")
41
+
42
+ if success and verbose:
43
+ env_logger.info(f"✅ Loaded environment file: /{env_path}")
44
+ elif not success:
45
+ # Try without leading slash (relative path)
46
+ success = load_dotenv(env_path)
47
+ if success and verbose:
48
+ env_logger.info(f"✅ Loaded environment file: {env_path}")
49
+ elif verbose:
50
+ env_logger.info(f"ℹ️ Environment file not found: {env_path}")
51
+
52
+ if verbose:
53
+ loggerplusplus.remove()
54
+
55
+ return success
56
+
57
+
58
+ def env(
59
+ key: str, *, default: Any = None, cast: type = str, required: bool = True
60
+ ) -> Any:
61
+ """
62
+ Read environment variable with optional type casting and default value.
63
+
64
+ Args:
65
+ key: Environment variable name
66
+ default: Default value if not found (default: None)
67
+ cast: Type to cast the value to (default: str)
68
+ required: Whether the variable is required (default: True)
69
+
70
+ Returns:
71
+ The environment variable value, cast to the specified type
72
+
73
+ Raises:
74
+ RuntimeError: If required variable is missing and no default provided
75
+
76
+ Examples:
77
+ >>> env("DATABASE_PORT", cast=int, default=5432)
78
+ 5432
79
+
80
+ >>> env("DEBUG_MODE", cast=bool, default=False)
81
+ False
82
+
83
+ >>> env("API_KEY") # Required by default
84
+ RuntimeError: missing required env var API_KEY
85
+ """
86
+ val = os.getenv(key, default)
87
+
88
+ if val is None:
89
+ if required:
90
+ raise RuntimeError(f"missing required env var {key}")
91
+ return None
92
+
93
+ # Special handling for boolean casting from string
94
+ if cast == bool and isinstance(val, str):
95
+ return val.strip().lower() not in {"false", "0", "no", ""}
96
+
97
+ return cast(val)
98
+
99
+
100
+ def env_optional(key: str, *, default: Any = None, cast: type = str) -> Any:
101
+ """
102
+ Read optional environment variable with type casting.
103
+
104
+ Convenience wrapper for env() with required=False.
105
+
106
+ Args:
107
+ key: Environment variable name
108
+ default: Default value if not found (default: None)
109
+ cast: Type to cast the value to (default: str)
110
+
111
+ Returns:
112
+ The environment variable value, cast to the specified type, or default
113
+
114
+ Example:
115
+ >>> env_optional("OPTIONAL_FEATURE", cast=bool, default=False)
116
+ False
117
+ """
118
+ return env(key, default=default, cast=cast, required=False)
@@ -0,0 +1,260 @@
1
+ """
2
+ YAML file based configuration loader
3
+ """
4
+
5
+ from typing import Any, Dict
6
+ import pathlib
7
+ import yaml
8
+ from loggerplusplus import loggerplusplus
9
+
10
+
11
+ class YamlConfigLoader:
12
+ """
13
+ Base class for YAML file based configuration.
14
+
15
+ This class requires instantiation with a path to a YAML file.
16
+ The YAML file is loaded in __init__, then __post_init__ is called
17
+ for custom parsing logic.
18
+
19
+ Features:
20
+ - Automatic YAML loading
21
+ - Post-initialization hook for custom parsing
22
+ - Access to raw config data
23
+ - Pretty printing support
24
+
25
+ Usage:
26
+ from configplusplus import YamlConfigLoader
27
+
28
+ class MyYamlConfig(YamlConfigLoader):
29
+ def __post_init__(self) -> None:
30
+ # Parse the loaded YAML data
31
+ self.database_host = self._raw_config["database"]["host"]
32
+ self.database_port = self._raw_config["database"]["port"]
33
+
34
+ # Parse nested structures
35
+ self.features = [
36
+ Feature(**feature_data)
37
+ for feature_data in self._raw_config["features"]
38
+ ]
39
+
40
+ # Instantiate with path
41
+ config = MyYamlConfig("config.yaml")
42
+ print(config.database_host)
43
+ print(config) # Pretty formatted output
44
+
45
+ YAML File Example:
46
+ database:
47
+ host: localhost
48
+ port: 5432
49
+
50
+ features:
51
+ - name: search
52
+ enabled: true
53
+ - name: export
54
+ enabled: false
55
+
56
+ Attributes:
57
+ config_path: Path to the loaded YAML file
58
+ _raw_config: Raw dictionary loaded from YAML
59
+ logger: LoggerPlusPlus logger instance
60
+ """
61
+
62
+ def __init__(self, config_path: str | pathlib.Path) -> None:
63
+ """
64
+ Initialize the YAML config loader.
65
+
66
+ Args:
67
+ config_path: Path to the YAML configuration file
68
+
69
+ Raises:
70
+ FileNotFoundError: If the configuration file doesn't exist
71
+ yaml.YAMLError: If the YAML file is invalid
72
+ """
73
+ # Setup logger
74
+ self.logger = loggerplusplus.bind(identifier=self.__class__.__name__)
75
+
76
+ # Convert to Path object
77
+ self.config_path = pathlib.Path(config_path)
78
+
79
+ # Validate file exists
80
+ if not self.config_path.exists():
81
+ msg = f"Configuration file not found: {self.config_path}"
82
+ self.logger.error(msg)
83
+ raise FileNotFoundError(msg)
84
+
85
+ # Load the YAML file
86
+ self._raw_config = self._load_yaml()
87
+ self.logger.debug(f"Loaded configuration from: {self.config_path}")
88
+
89
+ # Call the post-init hook for custom parsing
90
+ self.__post_init__()
91
+
92
+ def _load_yaml(self) -> Dict[str, Any]:
93
+ """
94
+ Load and parse the YAML configuration file.
95
+
96
+ Returns:
97
+ Dictionary containing the parsed YAML data
98
+
99
+ Raises:
100
+ yaml.YAMLError: If the YAML file is invalid
101
+ """
102
+ try:
103
+ with open(self.config_path, "r", encoding="utf-8") as f:
104
+ return yaml.safe_load(f)
105
+ except yaml.YAMLError as e:
106
+ self.logger.error(f"Failed to parse YAML file: {e}")
107
+ raise
108
+
109
+ def __post_init__(self) -> None:
110
+ """
111
+ Hook called after YAML file is loaded.
112
+
113
+ Override this method in subclasses to parse the loaded _raw_config
114
+ and set instance attributes.
115
+
116
+ Example:
117
+ def __post_init__(self) -> None:
118
+ self.database = DatabaseConfig(**self._raw_config["database"])
119
+ self.features = [
120
+ Feature(**f) for f in self._raw_config["features"]
121
+ ]
122
+ """
123
+ pass
124
+
125
+ def get(self, key: str, default: Any = None) -> Any:
126
+ """
127
+ Get a value from the raw config using dot notation.
128
+
129
+ Args:
130
+ key: Key in dot notation (e.g., "database.host")
131
+ default: Default value if key not found
132
+
133
+ Returns:
134
+ Value from config or default
135
+
136
+ Example:
137
+ >>> config.get("database.host")
138
+ "localhost"
139
+
140
+ >>> config.get("missing.key", default="fallback")
141
+ "fallback"
142
+ """
143
+ keys = key.split(".")
144
+ value = self._raw_config
145
+
146
+ try:
147
+ for k in keys:
148
+ value = value[k]
149
+ return value
150
+ except (KeyError, TypeError):
151
+ return default
152
+
153
+ def has(self, key: str) -> bool:
154
+ """
155
+ Check if a key exists in the raw config using dot notation.
156
+
157
+ Args:
158
+ key: Key in dot notation (e.g., "database.host")
159
+
160
+ Returns:
161
+ True if key exists, False otherwise
162
+
163
+ Example:
164
+ >>> config.has("database.host")
165
+ True
166
+
167
+ >>> config.has("missing.key")
168
+ False
169
+ """
170
+ keys = key.split(".")
171
+ value = self._raw_config
172
+
173
+ try:
174
+ for k in keys:
175
+ value = value[k]
176
+ return True
177
+ except (KeyError, TypeError):
178
+ return False
179
+
180
+ def to_dict(self) -> Dict[str, Any]:
181
+ """
182
+ Convert config to dictionary, excluding private/special attributes.
183
+
184
+ Returns:
185
+ Dictionary containing all public instance attributes
186
+ """
187
+ return {
188
+ k: v
189
+ for k, v in self.__dict__.items()
190
+ if not k.startswith("_") and k not in ("logger", "config_path")
191
+ }
192
+
193
+ def _mask_if_secret(self, key: str, value: Any) -> Any:
194
+ """
195
+ Mask potentially sensitive values.
196
+
197
+ Args:
198
+ key: Attribute name
199
+ value: Attribute value
200
+
201
+ Returns:
202
+ Masked value if sensitive, original value otherwise
203
+ """
204
+ if value is None:
205
+ return None
206
+
207
+ key_lower = key.lower()
208
+ sensitive_keywords = ("secret", "api_key", "password", "token", "credential")
209
+
210
+ if any(keyword in key_lower for keyword in sensitive_keywords):
211
+ s = str(value)
212
+ if len(s) <= 6:
213
+ return "***hidden***"
214
+ return f"{s[:3]}…{s[-2:]} (hidden)"
215
+
216
+ return value
217
+
218
+ def __repr__(self) -> str:
219
+ """
220
+ Pretty representation of the configuration.
221
+
222
+ Returns:
223
+ Formatted string with configuration display
224
+ """
225
+ lines = ["\n"]
226
+ lines.append("╔════════════════════════════════════════════╗")
227
+ lines.append(f"║ {self.__class__.__name__.upper().center(40)} ║")
228
+ lines.append("╚════════════════════════════════════════════╝")
229
+ lines.append("")
230
+ lines.append(f"▶ Config Path: {self.config_path}")
231
+ lines.append("")
232
+
233
+ config_dict = self.to_dict()
234
+ if not config_dict:
235
+ lines.append(" (No configuration loaded)")
236
+ else:
237
+ max_key_len = max(len(k) for k in config_dict.keys())
238
+
239
+ for key in sorted(config_dict.keys()):
240
+ value = config_dict[key]
241
+ display_value = self._mask_if_secret(key, value)
242
+
243
+ # Handle paths
244
+ if isinstance(display_value, pathlib.Path):
245
+ display_value = str(display_value.resolve())
246
+
247
+ # Handle lists/dicts - show count
248
+ if isinstance(display_value, list):
249
+ display_value = f"[{len(display_value)} items]"
250
+ elif isinstance(display_value, dict):
251
+ display_value = f"{{{len(display_value)} keys}}"
252
+
253
+ lines.append(f" {key.ljust(max_key_len)} = {display_value!r}")
254
+
255
+ lines.append("")
256
+ return "\n".join(lines)
257
+
258
+ def __str__(self) -> str:
259
+ """String representation uses the pretty repr."""
260
+ return self.__repr__()
@@ -0,0 +1,418 @@
1
+ Metadata-Version: 2.4
2
+ Name: configplusplus
3
+ Version: 0.1.0
4
+ Summary: A powerful configuration management library with beautiful display, environment variables and YAML support
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: config,configuration,environment,yaml,settings
8
+ Author: Florian BARRE
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Dist: loggerplusplus (>=1.0.5)
21
+ Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
22
+ Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
23
+ Project-URL: Documentation, https://github.com/Florian-BARRE/ConfigPlusPlus#readme
24
+ Project-URL: Homepage, https://github.com/Florian-BARRE/ConfigPlusPlus
25
+ Project-URL: Repository, https://github.com/Florian-BARRE/ConfigPlusPlus
26
+ Description-Content-Type: text/markdown
27
+
28
+ # ConfigPlusPlus
29
+
30
+ > Beautiful configuration management for Python with environment variables and YAML support
31
+
32
+ [![PyPI version](https://badge.fury.io/py/configplusplus.svg)](https://pypi.org/project/configplusplus/)
33
+ [![Python](https://img.shields.io/pypi/pyversions/configplusplus.svg)](https://pypi.org/project/configplusplus/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
+
36
+ ## Features
37
+
38
+ ✨ **Beautiful Display** - Pretty formatted configuration output with automatic grouping and secret masking
39
+
40
+ 🔐 **Secret Masking** - Automatically hides sensitive values (API keys, passwords, tokens)
41
+
42
+ 🌍 **Environment Variables** - Load configuration from environment variables with type casting
43
+
44
+ 📄 **YAML Support** - Load configuration from YAML files with custom parsing
45
+
46
+ 🎯 **Type Casting** - Automatic type conversion (str, int, float, bool, Path)
47
+
48
+ 🏷️ **Static & Instance** - Support for both static class-based and instance-based configs
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install configplusplus
54
+ ```
55
+
56
+ Or with Poetry:
57
+
58
+ ```bash
59
+ poetry add configplusplus
60
+ ```
61
+
62
+ ## Quick Start
63
+
64
+ ### Environment-Based Configuration
65
+
66
+ ```python
67
+ from configplusplus import EnvConfigLoader, env
68
+ import pathlib
69
+
70
+ class AppConfig(EnvConfigLoader):
71
+ # Required variables
72
+ DATABASE_HOST = env("DATABASE_HOST")
73
+ DATABASE_PORT = env("DATABASE_PORT", cast=int)
74
+
75
+ # Optional with defaults
76
+ DEBUG_MODE = env("DEBUG_MODE", cast=bool, default=False)
77
+
78
+ # Paths
79
+ DATA_DIR = env("DATA_DIR", cast=pathlib.Path)
80
+
81
+ # Secrets (automatically masked in output)
82
+ SECRET_API_KEY = env("SECRET_API_KEY")
83
+
84
+ # Use as static class
85
+ print(AppConfig.DATABASE_HOST)
86
+ print(AppConfig) # Beautiful formatted output
87
+ ```
88
+
89
+ **Output:**
90
+ ```
91
+ ╔════════════════════════════════════════════╗
92
+ ║ APPCONFIG ║
93
+ ╚════════════════════════════════════════════╝
94
+
95
+ ▶ DATABASE
96
+ DATABASE_HOST = 'localhost'
97
+ DATABASE_PORT = 5432
98
+
99
+ ▶ DEBUG
100
+ DEBUG_MODE = False
101
+
102
+ ▶ DATA
103
+ DATA_DIR = '/var/data/myapp'
104
+
105
+ ▶ SECRET
106
+ SECRET_API_KEY = 'sec...et (hidden)'
107
+ ```
108
+
109
+ ### YAML-Based Configuration
110
+
111
+ ```python
112
+ from configplusplus import YamlConfigLoader
113
+
114
+ class UiConfig(YamlConfigLoader):
115
+ def __post_init__(self) -> None:
116
+ # Parse the loaded YAML data
117
+ self.app_name = self._raw_config["application"]["name"]
118
+ self.theme = self._raw_config["display"]["theme"]
119
+
120
+ # Parse nested structures
121
+ self.filters = [
122
+ FilterConfig(**f)
123
+ for f in self._raw_config["filters"]
124
+ ]
125
+
126
+ # Instantiate with path
127
+ config = UiConfig("config.yaml")
128
+ print(config.app_name)
129
+ print(config) # Beautiful formatted output
130
+ ```
131
+
132
+ ## Environment Variables
133
+
134
+ ### Basic Usage
135
+
136
+ ```python
137
+ from configplusplus import env
138
+
139
+ # String (default)
140
+ DATABASE_HOST = env("DATABASE_HOST")
141
+
142
+ # Integer
143
+ DATABASE_PORT = env("DATABASE_PORT", cast=int)
144
+
145
+ # Boolean
146
+ DEBUG_MODE = env("DEBUG_MODE", cast=bool)
147
+
148
+ # Float
149
+ TEMPERATURE = env("TEMPERATURE", cast=float)
150
+
151
+ # Path
152
+ DATA_DIR = env("DATA_DIR", cast=pathlib.Path)
153
+
154
+ # With default value
155
+ TIMEOUT = env("TIMEOUT", cast=int, default=30)
156
+
157
+ # Optional (won't raise if missing)
158
+ OPTIONAL = env("OPTIONAL", required=False, default=None)
159
+ ```
160
+
161
+ ### Boolean Casting
162
+
163
+ When `cast=bool`, these strings are considered `False`:
164
+ - `"false"`, `"False"`, `"FALSE"`
165
+ - `"0"`
166
+ - `"no"`, `"No"`, `"NO"`
167
+ - `""` (empty string)
168
+
169
+ All other values are considered `True`.
170
+
171
+ ### Loading .env Files
172
+
173
+ ```python
174
+ from configplusplus import safe_load_envs
175
+
176
+ # Load .env file with logging
177
+ safe_load_envs() # Loads from ".env"
178
+
179
+ # Load from custom path
180
+ safe_load_envs("config/.env")
181
+
182
+ # Silent loading
183
+ safe_load_envs(verbose=False)
184
+ ```
185
+
186
+ ## YAML Configuration
187
+
188
+ ### Basic Usage
189
+
190
+ ```python
191
+ from configplusplus import YamlConfigLoader
192
+
193
+ class MyConfig(YamlConfigLoader):
194
+ def __post_init__(self) -> None:
195
+ # Access raw YAML data
196
+ self.database_host = self._raw_config["database"]["host"]
197
+ self.database_port = self._raw_config["database"]["port"]
198
+ ```
199
+
200
+ ### Helper Methods
201
+
202
+ ```python
203
+ config = MyConfig("config.yaml")
204
+
205
+ # Get values with dot notation
206
+ host = config.get("database.host")
207
+ port = config.get("database.port")
208
+
209
+ # Get with default
210
+ timeout = config.get("api.timeout", default=30)
211
+
212
+ # Check if key exists
213
+ if config.has("database.host"):
214
+ print("Database configured")
215
+
216
+ # Convert to dictionary
217
+ config_dict = config.to_dict()
218
+ ```
219
+
220
+ ## Advanced Features
221
+
222
+ ### Custom Validation
223
+
224
+ ```python
225
+ class ValidatedConfig(EnvConfigLoader):
226
+ DATABASE_PORT = env("DATABASE_PORT", cast=int)
227
+
228
+ @classmethod
229
+ def validate(cls) -> None:
230
+ super().validate()
231
+ if cls.DATABASE_PORT < 1024:
232
+ raise RuntimeError("DATABASE_PORT must be >= 1024")
233
+
234
+ # Validate configuration
235
+ ValidatedConfig.validate()
236
+ ```
237
+
238
+ ### Structured Data from YAML
239
+
240
+ ```python
241
+ from dataclasses import dataclass
242
+ from typing import List
243
+
244
+ @dataclass
245
+ class FilterConfig:
246
+ name: str
247
+ type: str
248
+ enabled: bool = True
249
+
250
+ class UiConfig(YamlConfigLoader):
251
+ def __post_init__(self) -> None:
252
+ # Parse list of structured objects
253
+ self.filters: List[FilterConfig] = [
254
+ FilterConfig(**f)
255
+ for f in self._raw_config["filters"]
256
+ ]
257
+ ```
258
+
259
+ ### Multiple Configuration Sources
260
+
261
+ ```python
262
+ # Combine environment and YAML configs
263
+ class AppConfig(EnvConfigLoader):
264
+ # From environment
265
+ SECRET_API_KEY = env("SECRET_API_KEY")
266
+ DATABASE_HOST = env("DATABASE_HOST")
267
+
268
+ # Load YAML for features
269
+ @classmethod
270
+ def load_features(cls) -> None:
271
+ yaml_config = YamlConfigLoader("features.yaml")
272
+ cls.FEATURES = yaml_config.get("features")
273
+
274
+ AppConfig.load_features()
275
+ ```
276
+
277
+ ## Secret Masking
278
+
279
+ Variables containing these keywords are automatically masked in output:
280
+ - `SECRET`
281
+ - `API_KEY`
282
+ - `PASSWORD`
283
+ - `TOKEN`
284
+ - `CREDENTIAL`
285
+
286
+ Example:
287
+ ```python
288
+ SECRET_API_KEY = "sk_live_abc123xyz789"
289
+ # Output: "sk_...89 (hidden)"
290
+
291
+ PASSWORD = "short"
292
+ # Output: "***hidden***"
293
+ ```
294
+
295
+ ## Configuration Grouping
296
+
297
+ Configuration values are automatically grouped by prefix:
298
+
299
+ ```python
300
+ class AppConfig(EnvConfigLoader):
301
+ DATABASE_HOST = env("DATABASE_HOST")
302
+ DATABASE_PORT = env("DATABASE_PORT", cast=int)
303
+ API_ENDPOINT = env("API_ENDPOINT")
304
+ API_KEY = env("API_KEY")
305
+ ```
306
+
307
+ **Output shows grouped display:**
308
+ ```
309
+ ▶ DATABASE
310
+ DATABASE_HOST = 'localhost'
311
+ DATABASE_PORT = 5432
312
+
313
+ ▶ API
314
+ API_ENDPOINT = 'https://api.example.com'
315
+ API_KEY = 'key...23 (hidden)'
316
+ ```
317
+
318
+ ## Real-World Examples
319
+
320
+ ### FastAPI Application Config
321
+
322
+ ```python
323
+ from configplusplus import EnvConfigLoader, env, safe_load_envs
324
+ import pathlib
325
+
326
+ safe_load_envs()
327
+
328
+ class APIConfig(EnvConfigLoader):
329
+ # Server
330
+ HOST = env("HOST", default="0.0.0.0")
331
+ PORT = env("PORT", cast=int, default=8000)
332
+
333
+ # Database
334
+ DATABASE_URL = env("DATABASE_URL")
335
+ DATABASE_POOL_SIZE = env("DATABASE_POOL_SIZE", cast=int, default=10)
336
+
337
+ # Redis
338
+ REDIS_HOST = env("REDIS_HOST", default="localhost")
339
+ REDIS_PORT = env("REDIS_PORT", cast=int, default=6379)
340
+
341
+ # Security
342
+ SECRET_JWT_KEY = env("SECRET_JWT_KEY")
343
+ TOKEN_EXPIRE_MINUTES = env("TOKEN_EXPIRE_MINUTES", cast=int, default=60)
344
+
345
+ # Features
346
+ ENABLE_CORS = env("ENABLE_CORS", cast=bool, default=True)
347
+ ENABLE_DOCS = env("ENABLE_DOCS", cast=bool, default=False)
348
+
349
+ @classmethod
350
+ def validate(cls) -> None:
351
+ if cls.PORT < 1024 or cls.PORT > 65535:
352
+ raise RuntimeError("Invalid PORT")
353
+
354
+ # Use in FastAPI
355
+ from fastapi import FastAPI
356
+
357
+ app = FastAPI(
358
+ title="My API",
359
+ docs_url="/docs" if APIConfig.ENABLE_DOCS else None,
360
+ )
361
+ ```
362
+
363
+ ### Document Processing Pipeline Config
364
+
365
+ ```python
366
+ from configplusplus import YamlConfigLoader
367
+ from typing import List
368
+ from dataclasses import dataclass
369
+
370
+ @dataclass
371
+ class ProcessorConfig:
372
+ name: str
373
+ enabled: bool
374
+ priority: int
375
+
376
+ class PipelineConfig(YamlConfigLoader):
377
+ def __post_init__(self) -> None:
378
+ # Parse processors
379
+ self.processors: List[ProcessorConfig] = [
380
+ ProcessorConfig(**p)
381
+ for p in self._raw_config["processors"]
382
+ ]
383
+
384
+ # Parse paths
385
+ self.input_dir = pathlib.Path(self._raw_config["paths"]["input"])
386
+ self.output_dir = pathlib.Path(self._raw_config["paths"]["output"])
387
+
388
+ # Parse settings
389
+ self.batch_size = self._raw_config["settings"]["batch_size"]
390
+ self.max_workers = self._raw_config["settings"]["max_workers"]
391
+
392
+ # Load configuration
393
+ config = PipelineConfig("pipeline.yaml")
394
+
395
+ # Use in pipeline
396
+ for processor in sorted(config.processors, key=lambda x: x.priority):
397
+ if processor.enabled:
398
+ print(f"Running {processor.name}")
399
+ ```
400
+
401
+ ## Documentation
402
+
403
+ - **Quick Reference**: See [REFERENCE.md](REFERENCE.md) for a cheat sheet
404
+ - **Detailed Guide**: See [USAGE.md](USAGE.md) for comprehensive documentation
405
+ - **Examples**: Check the `examples/` directory for working code samples
406
+
407
+ ## Links
408
+
409
+ - **PyPI**: https://pypi.org/project/configplusplus/
410
+ - **GitHub**: https://github.com/Florian-BARRE/ConfigPlusPlus
411
+ - **Issues**: https://github.com/Florian-BARRE/ConfigPlusPlus/issues
412
+
413
+ ## License
414
+
415
+ MIT License - See [LICENSE](LICENSE) file for details.
416
+
417
+ **Author**: Florian BARRE
418
+
@@ -0,0 +1,9 @@
1
+ configplusplus/__init__.py,sha256=X0kpTKmnieSUJGAteRcqc9ALIyN-YZH6060vnKvCboM,473
2
+ configplusplus/base.py,sha256=t4pFLrwRefdMVQjQRfsslgxvZcupRQ8HOeYC1P3vm-U,4317
3
+ configplusplus/env_loader.py,sha256=yFgZs0Rk93m4Ai8VwjlmS84DMxll61FR0ywwHNLx-CU,3846
4
+ configplusplus/utils.py,sha256=-G_GmsF2wij66cmHAZPrPGErffVyaqO0jkfm3haU5JE,3363
5
+ configplusplus/yaml_loader.py,sha256=HSATQWmUzi2K20ZfV-AkvT5MY84A1WUqcKIizJSvWhc,7907
6
+ configplusplus-0.1.0.dist-info/METADATA,sha256=hBBVJ3E5oHShrmUdT5ZztoK0vXzgeRKYdVTsmR9iK2U,10658
7
+ configplusplus-0.1.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
8
+ configplusplus-0.1.0.dist-info/licenses/LICENSE,sha256=DalX5FRVQEaFnrjMMLLqNh2NAUk3ZHHjA_uAE7JdIlk,1070
9
+ configplusplus-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.3.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Florian BARRE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.