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.
- configplusplus/__init__.py +20 -0
- configplusplus/base.py +138 -0
- configplusplus/env_loader.py +134 -0
- configplusplus/utils.py +118 -0
- configplusplus/yaml_loader.py +260 -0
- configplusplus-0.1.0.dist-info/METADATA +418 -0
- configplusplus-0.1.0.dist-info/RECORD +9 -0
- configplusplus-0.1.0.dist-info/WHEEL +4 -0
- configplusplus-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
configplusplus/utils.py
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/configplusplus/)
|
|
33
|
+
[](https://pypi.org/project/configplusplus/)
|
|
34
|
+
[](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,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.
|