confkit 0.1.1__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.
confkit/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """Module that provides the main interface for the configurator package.
2
+
3
+ It includes the Config class and various data types used for configuration values.
4
+ """
5
+
6
+ from .config import Config
7
+ from .data_types import (
8
+ BaseDataType,
9
+ Boolean,
10
+ Enum,
11
+ Float,
12
+ Integer,
13
+ IntEnum,
14
+ IntFlag,
15
+ NoneType,
16
+ Optional,
17
+ StrEnum,
18
+ String,
19
+ )
20
+ from .exceptions import InvalidConverterError, InvalidDefaultError
21
+
22
+ __all__ = [
23
+ "BaseDataType",
24
+ "Boolean",
25
+ "Config",
26
+ "Enum",
27
+ "Float",
28
+ "IntEnum",
29
+ "IntFlag",
30
+ "Integer",
31
+ "InvalidConverterError",
32
+ "InvalidDefaultError",
33
+ "NoneType",
34
+ "Optional",
35
+ "StrEnum",
36
+ "String",
37
+ ]
confkit/config.py ADDED
@@ -0,0 +1,299 @@
1
+ """Module for a config descriptor.
2
+
3
+ The Config descriptor is used to read and write config values from a ConfigParser object.
4
+ It is used to create a descriptor for config values, preserving type information.
5
+ It also provides a way to set default values and to set config values using decorators.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from functools import wraps
10
+ from typing import TYPE_CHECKING, ClassVar, cast, overload
11
+
12
+ from .data_types import BaseDataType, Boolean, Float, Integer, NoneType, Optional, String
13
+ from .exceptions import InvalidConverterError, InvalidDefaultError
14
+ from .sentinels import UNSET
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Callable
18
+ from configparser import ConfigParser
19
+ from pathlib import Path
20
+
21
+
22
+ class Config[VT]:
23
+ """A descriptor for config values, preserving type information.
24
+
25
+ the ValueType (VT) is the type you want the config value to be.
26
+ """
27
+
28
+ validate_types: ClassVar[bool] = True # Validate that the converter returns the same type as the default value. (not strict)
29
+ write_on_edit: ClassVar[bool] = True # Write to the config file when updating a value.
30
+ optional: bool = False # if True, allows None as an extra type when validating types. (both instance and class variables.)
31
+
32
+ _parser: ConfigParser = UNSET
33
+ _file: Path = UNSET
34
+ _has_read_config: bool = False
35
+
36
+ if TYPE_CHECKING:
37
+ # Overloads for type checkers to understand the different settings of the Config descriptors.
38
+ @overload
39
+ def __init__(self: Config[str], default: str) -> None: ...
40
+ @overload
41
+ def __init__(self: Config[None], default: None) -> None: ...
42
+ @overload
43
+ def __init__(self: Config[bool], default: bool) -> None: ... # noqa: FBT001
44
+ @overload
45
+ def __init__(self: Config[int], default: int) -> None: ...
46
+ @overload
47
+ def __init__(self: Config[float], default: float) -> None: ...
48
+ @overload
49
+ def __init__(self: Config[str | None], default: str, *, optional: bool) -> None: ...
50
+ @overload
51
+ def __init__(self: Config[None], default: None, *, optional: bool) -> None: ...
52
+ @overload
53
+ def __init__(self: Config[bool | None], default: bool, *, optional: bool) -> None: ... # noqa: FBT001
54
+ @overload
55
+ def __init__(self: Config[int | None], default: int, *, optional: bool) -> None: ...
56
+ @overload
57
+ def __init__(self: Config[float | None], default: float, *, optional: bool) -> None: ...
58
+ @overload # Custom data type, like Enum's or custom class.
59
+ def __init__(self, default: BaseDataType[VT]) -> None: ...
60
+
61
+ # type Complains about the self and default overloads for None and str
62
+ # they are explicitly set for type checkers, the actual representation doesn't matter
63
+ # in runtime, as VT is allowed to be any type.
64
+ def __init__( # type: ignore[reportInconsistentOverload]
65
+ self,
66
+ default: VT | None | BaseDataType[VT] = UNSET,
67
+ *,
68
+ optional: bool = False,
69
+ ) -> None:
70
+ """Initialize the config descriptor with a default value.
71
+
72
+ Validate that parser and filepath are present.
73
+ """
74
+ self.optional = optional or Config.optional # Be truthy when either one is true.
75
+
76
+ if not self.optional and default is UNSET:
77
+ msg = "Default value cannot be None when optional is False."
78
+ raise InvalidDefaultError(msg)
79
+
80
+ self._initialize_data_type(default)
81
+ self._validate_init()
82
+ self._read_parser()
83
+
84
+ def _initialize_data_type(self, default: VT | None | BaseDataType[VT]) -> None:
85
+ """Initialize the data type based on the default value."""
86
+ if not self.optional and default is not None:
87
+ self._data_type = self._cast_data_type(default)
88
+ else:
89
+ self._data_type = self._cast_optional_data_type(default)
90
+
91
+ def _cast_optional_data_type(self, default: VT | None | BaseDataType[VT]) -> BaseDataType[VT | None]:
92
+ """Convert the default value to an Optional data type."""
93
+ if default is None:
94
+ return cast("BaseDataType[VT | None]", NoneType())
95
+ return Optional(self._cast_data_type(default))
96
+
97
+ def _cast_data_type(self, default: VT | BaseDataType[VT]) -> BaseDataType[VT]:
98
+ """Convert the default value to a BaseDataType."""
99
+ # We use Cast to shut up type checkers, as we know primitive types will be correct.
100
+ # If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types.
101
+ match default:
102
+ case bool():
103
+ data_type = cast("BaseDataType[VT]", Boolean(default))
104
+ case None:
105
+ data_type = cast("BaseDataType[VT]", NoneType())
106
+ case int():
107
+ data_type = cast("BaseDataType[VT]", Integer(default))
108
+ case float():
109
+ data_type = cast("BaseDataType[VT]", Float(default))
110
+ case str():
111
+ data_type = cast("BaseDataType[VT]", String(default))
112
+ case BaseDataType():
113
+ data_type = default
114
+ case _:
115
+ msg = (
116
+ f"Unsupported default value type: {type(default).__name__}. "
117
+ "Use a BaseDataType subclass for custom types."
118
+ )
119
+ raise InvalidDefaultError(msg)
120
+ return data_type
121
+
122
+ def _read_parser(self) -> None:
123
+ """Ensure the parser has read the file at initialization. Avoids rewriting the file when settings are already set."""
124
+ if not self._has_read_config:
125
+ Config._parser.read(Config._file)
126
+ Config._has_read_config = True
127
+
128
+ def _validate_init(self) -> None:
129
+ """Validate the config descriptor, ensuring it's properly set up."""
130
+ self.validate_file()
131
+ self.validate_parser()
132
+
133
+ def convert(self, value: str) -> VT:
134
+ """Convert the value to the desired type using the given converter method."""
135
+ # Ignore the type errror of VT, type checkers don't like None as an option
136
+ # We handle it using the `optional` flag. so we can safely ignore it.
137
+ return self._data_type.convert(value) # type: ignore[reportReturnType]
138
+
139
+ @staticmethod
140
+ def set_parser(parser: ConfigParser) -> None:
141
+ """Set the parser for ALL descriptors."""
142
+ Config._parser = parser
143
+
144
+ @staticmethod
145
+ def set_file(file: Path) -> None:
146
+ """Set the file for ALL descriptors."""
147
+ Config._file = file
148
+
149
+ def validate_strict_type(self) -> None:
150
+ """Validate the type of the converter matches the desired type."""
151
+ if not Config.validate_types:
152
+ return
153
+ if self._data_type.convert is UNSET:
154
+ msg = "Converter is not set."
155
+ raise InvalidConverterError(msg)
156
+
157
+ config_value = Config._parser.get(self._section, self._setting)
158
+ converted_value = self.convert(config_value)
159
+ converted_type = type(converted_value)
160
+
161
+ if not self._data_type.validate():
162
+ msg = f"Invalid value for {self._section}.{self._setting}: {converted_value}"
163
+ raise InvalidConverterError(msg)
164
+
165
+ if self.optional and converted_type in (type(self._data_type.default), type(None)):
166
+ # Allow None or the same type as the default value to be returned by the converter when _optional is True.
167
+ return
168
+ if type(converted_value) is not type(self._data_type.default):
169
+ msg = f"Converter does not return the same type as the default value <{type(self._data_type.default)}>."
170
+ raise InvalidConverterError(msg)
171
+
172
+ def validate_file(self) -> None:
173
+ """Validate the config file."""
174
+ if Config._file is UNSET:
175
+ msg = f"Config file is not set. use {Config.__class__.__name__}.set_file to set it."
176
+ raise ValueError(msg)
177
+
178
+ def validate_parser(self) -> None:
179
+ """Validate the config parser."""
180
+ if Config._parser is UNSET:
181
+ msg = f"Config parser is not set. use {Config.__class__.__name__}.set_parser to set it."
182
+ raise ValueError(msg)
183
+
184
+ def __set_name__(self, owner: type, name: str) -> None:
185
+ """Set the name of the attribute to the name of the descriptor."""
186
+ self.name = name
187
+ self._section = owner.__name__
188
+ self._setting = name
189
+ self._ensure_option()
190
+ self._original_value = Config._parser.get(self._section, self._setting) or self._data_type.default
191
+ self.private = f"_{self._section}_{self._setting}_{self.name}"
192
+
193
+ def _ensure_section(self) -> None:
194
+ """Ensure the section exists in the config file. Creates one if it doesn't exist."""
195
+ if not self._parser.has_section(self._section):
196
+ self._parser.add_section(self._section)
197
+
198
+ def _ensure_option(self) -> None:
199
+ """Ensure the option exists in the config file. Creates one if it doesn't exist."""
200
+ self._ensure_section()
201
+ if not self._parser.has_option(self._section, self._setting):
202
+ Config._set(self._section, self._setting, self._data_type.default)
203
+
204
+ def __get__(self, obj: object, obj_type: object) -> VT:
205
+ """Get the value of the attribute."""
206
+ # obj_type is the class in which the variable is defined
207
+ # so it can be different than type of VT
208
+ # but we don't need obj or it's type to get the value from config in our case.
209
+ self.validate_strict_type()
210
+ return self.convert(Config._parser.get(self._section, self._setting))
211
+
212
+ def __set__(self, obj: object, value: VT) -> None:
213
+ """Set the value of the attribute."""
214
+ Config._set(self._section, self._setting, value)
215
+ setattr(obj, self.private, value)
216
+
217
+ @staticmethod
218
+ def _sanitize_str(value: str) -> str:
219
+ """Escape the percent sign in the value."""
220
+ return value.replace("%", "%%")
221
+
222
+ @staticmethod
223
+ def _set(section: str, setting: str, value: VT) -> None:
224
+ """Set a config value, and write it to the file."""
225
+ if not Config._parser.has_section(section):
226
+ Config._parser.add_section(section)
227
+ sanitized_str = Config._sanitize_str(str(value))
228
+ Config._parser.set(section, setting, sanitized_str)
229
+ if Config.write_on_edit:
230
+ with Config._file.open("w") as f:
231
+ Config._parser.write(f)
232
+
233
+ @staticmethod
234
+ def set(section: str, setting: str, value: VT): # noqa: ANN205
235
+ """Set a config value using this descriptor."""
236
+
237
+ def wrapper[F, **P](func: Callable[P, F]) -> Callable[P, F]:
238
+ @wraps(func)
239
+ def inner(*args: P.args, **kwargs: P.kwargs) -> F:
240
+ Config._set(section, setting, value)
241
+ return func(*args, **kwargs)
242
+
243
+ return inner
244
+ return wrapper
245
+
246
+ @staticmethod
247
+ def with_setting[OVT](setting: Config[OVT]): # noqa: ANN205
248
+ """Insert a config value into **kwargs to a given method/function using this decorator."""
249
+ def wrapper[F, **P](func: Callable[P, F]) -> Callable[P, F]:
250
+ @wraps(func)
251
+ def inner(*args: P.args, **kwargs: P.kwargs) -> F:
252
+ kwargs[setting.name] = setting.convert(Config._parser.get(setting._section, setting._setting))
253
+ return func(*args, **kwargs)
254
+
255
+ return inner
256
+ return wrapper
257
+
258
+ @staticmethod
259
+ def as_kwarg(section: str, setting: str, name: str | None = None, default: VT = UNSET): # noqa: ANN205
260
+ """Insert a config value into **kwargs to a given method/function using this descriptor.
261
+
262
+ Use kwarg.get(`name`) to get the value.
263
+ `name` is the name the kwarg gets if passed, if None, it will be the same as `setting`.
264
+ Section parameter is just for finding the config value.
265
+ """
266
+ if name is None:
267
+ name = setting
268
+ if default is UNSET and not Config._parser.has_option(section, setting):
269
+ msg = f"Config value {section=} {setting=} is not set. and no default value is given."
270
+ raise ValueError(msg)
271
+
272
+ def wrapper[F, **P](func: Callable[P, F]) -> Callable[P, F]:
273
+ @wraps(func)
274
+ def inner(*args: P.args, **kwargs: P.kwargs) -> F:
275
+ if default is not UNSET:
276
+ Config._set_default(section, setting, default)
277
+ kwargs[name] = Config._parser.get(section, setting) # ty: ignore[call-non-callable]
278
+ return func(*args, **kwargs)
279
+
280
+ return inner
281
+ return wrapper
282
+
283
+ @staticmethod
284
+ def _set_default(section: str, setting: str, value: VT) -> None:
285
+ if Config._parser.get(section, setting, fallback=UNSET) is UNSET:
286
+ Config._set(section, setting, value)
287
+
288
+ @staticmethod
289
+ def default(section: str, setting: str, value: VT): # noqa: ANN205
290
+ """Set a default config value if none are set yet using this descriptor."""
291
+
292
+ def wrapper[F, **P](func: Callable[P, F]) -> Callable[P, F]:
293
+ @wraps(func)
294
+ def inner(*args: P.args, **kwargs: P.kwargs) -> F:
295
+ Config._set_default(section, setting, value)
296
+ return func(*args, **kwargs)
297
+
298
+ return inner
299
+ return wrapper
confkit/data_types.py ADDED
@@ -0,0 +1,154 @@
1
+ """Module that contains the base data types used in the config system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from abc import ABC, abstractmethod
7
+
8
+ from .exceptions import InvalidConverterError
9
+
10
+
11
+ class BaseDataType[T](ABC):
12
+ """Base class used for Config descriptors to define a data type."""
13
+
14
+ def __init__(self, default: T) -> None:
15
+ """Initialize the base data type."""
16
+ self.default = default
17
+ self.value = default
18
+
19
+ @abstractmethod
20
+ def convert(self, value: str) -> T:
21
+ """Convert a string value to the desired type."""
22
+
23
+ def validate(self) -> bool:
24
+ """Validate that the value matches the expected type."""
25
+ orig_bases: tuple[type, ...] | None = getattr(self.__class__, "__orig_bases__", None)
26
+
27
+ if not orig_bases:
28
+ msg = "No type information available for validation."
29
+ raise InvalidConverterError(msg)
30
+
31
+ # Extract type arguments from the generic base
32
+ for base in orig_bases:
33
+ if hasattr(base, "__args__"):
34
+ type_args = base.__args__
35
+ if type_args:
36
+ for type_arg in type_args:
37
+ if isinstance(self.value, type_arg):
38
+ return True
39
+ msg = f"Value {self.value} is not any of {type_args}."
40
+ raise InvalidConverterError(msg)
41
+ msg = "This should not have raised. Report to the library maintainers with code: `DTBDT38`"
42
+ raise TypeError(msg)
43
+
44
+ class Enum(BaseDataType[enum.Enum]):
45
+ """A config value that is an enum."""
46
+
47
+ def convert(self, value: str) -> enum.Enum:
48
+ """Convert a string value to an enum."""
49
+ parsed_enum_name = value.split(".")[-1]
50
+ return self.value.__class__[parsed_enum_name]
51
+
52
+ class StrEnum(BaseDataType[enum.StrEnum]):
53
+ """A config value that is an enum."""
54
+
55
+ def convert(self, value: str) -> enum.StrEnum:
56
+ """Convert a string value to an enum."""
57
+ return self.value.__class__(value)
58
+
59
+ class IntEnum(BaseDataType[enum.IntEnum]):
60
+ """A config value that is an enum."""
61
+
62
+ def convert(self, value: str) -> enum.IntEnum:
63
+ """Convert a string value to an enum."""
64
+ return self.value.__class__(int(value))
65
+
66
+ class IntFlag(BaseDataType[enum.IntFlag]):
67
+ """A config value that is an enum."""
68
+
69
+ def convert(self, value: str) -> enum.IntFlag:
70
+ """Convert a string value to an enum."""
71
+ return self.value.__class__(int(value))
72
+
73
+ class NoneType(BaseDataType[None]):
74
+ """A config value that is None."""
75
+
76
+ def __init__(self) -> None:
77
+ """Initialize the NoneType data type."""
78
+ super().__init__(None)
79
+
80
+ def convert(self, value: str) -> None:
81
+ """Convert a string value to None."""
82
+ if value.lower().strip() in {"none", "null", "nil", ""}:
83
+ return
84
+ msg = f"Cannot convert {value} to None."
85
+ raise ValueError(msg)
86
+
87
+
88
+ class String(BaseDataType[str]):
89
+ """A config value that is a string."""
90
+
91
+ def convert(self, value: str) -> str:
92
+ """Convert a string value to a string."""
93
+ return value
94
+
95
+
96
+ class Float(BaseDataType[float]):
97
+ """A config value that is a float."""
98
+
99
+ def convert(self, value: str) -> float:
100
+ """Convert a string value to a float."""
101
+ return float(value)
102
+
103
+
104
+ class Boolean(BaseDataType[bool]):
105
+ """A config value that is a boolean."""
106
+
107
+ def convert(self, value: str) -> bool:
108
+ """Convert a string value to a boolean."""
109
+ if value.lower() in {"true", "1", "yes"}:
110
+ return True
111
+ if value.lower() in {"false", "0", "no"}:
112
+ return False
113
+ msg = f"Cannot convert {value} to boolean."
114
+ raise ValueError(msg)
115
+
116
+
117
+ class Integer(BaseDataType[int]):
118
+ """A config value that is an integer."""
119
+
120
+ def convert(self, value: str) -> int:
121
+ """Convert a string value to an integer."""
122
+ return int(value)
123
+
124
+ class Optional[T](BaseDataType[T | None]):
125
+ """A config value that is optional, can be None or a specific type."""
126
+
127
+ _none_type = NoneType()
128
+
129
+ def __init__(self, data_type: BaseDataType[T]) -> None:
130
+ """Initialize the optional data type. Wrapping the provided data type."""
131
+ self._data_type = data_type
132
+
133
+ @property
134
+ def default(self) -> T | None:
135
+ """Get the default value of the wrapped data type."""
136
+ return self._data_type.default
137
+
138
+ @property
139
+ def value(self) -> T | None:
140
+ """Get the current value of the wrapped data type."""
141
+ return self._data_type.value
142
+
143
+ def convert(self, value: str) -> T | None:
144
+ """Convert a string value to the optional type."""
145
+ try:
146
+ return self._none_type.convert(value)
147
+ except ValueError:
148
+ return self._data_type.convert(value)
149
+
150
+ def validate(self) -> bool:
151
+ """Validate that the value is of the wrapped data type or None."""
152
+ if self._data_type.value is None:
153
+ return True
154
+ return self._data_type.validate()
confkit/exceptions.py ADDED
@@ -0,0 +1,8 @@
1
+ """Module for custom exceptions used in the configurator package."""
2
+
3
+ class InvalidDefaultError(ValueError):
4
+ """Raised when the default value is not set or invalid."""
5
+
6
+
7
+ class InvalidConverterError(ValueError):
8
+ """Raised when the converter is not set or invalid."""
confkit/py.typed ADDED
File without changes
confkit/sentinels.py ADDED
@@ -0,0 +1,21 @@
1
+ """Private sentinel for missing values."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class _MissingSentinel:
7
+ __slots__ = ()
8
+
9
+ def __eq__(self, other: object) -> bool:
10
+ return False
11
+
12
+ def __bool__(self) -> bool:
13
+ return False
14
+
15
+ def __hash__(self) -> int:
16
+ return 0
17
+
18
+ def __repr__(self) -> str:
19
+ return "MISSING"
20
+
21
+ UNSET: Any = _MissingSentinel()
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: confkit
3
+ Version: 0.1.1
4
+ Summary: Lightweight and Easy to use configuration manager for Python projects
5
+ Author-email: HEROgold <martijnwieringa28@gmail.com>
6
+ Requires-Python: >=3.13
7
+ Description-Content-Type: text/markdown
8
+
9
+ # confkit
10
+
11
+ [![Test](https://github.com/HEROgold/confkit/actions/workflows/test.yml/badge.svg)](https://github.com/HEROgold/confkit/actions/workflows/test.yml)
12
+
13
+ Type-safe configuration manager for Python projects using descriptors and ConfigParser.
14
+
15
+ ## What is it?
16
+
17
+ confkit is a Python library that provides type-safe configuration management with automatic type conversion and validation.
18
+ It uses descriptors to define configuration values as class attributes that automatically read from and write to INI files.
19
+
20
+ ## What does it do?
21
+
22
+ - Type-safe configuration with automatic type conversion
23
+ - Automatic INI file management
24
+ - Default value handling with file persistence
25
+ - Optional value support
26
+ - Enum support (Enum, StrEnum, IntEnum, IntFlag)
27
+ - Method decorators for injecting configuration values
28
+ - Runtime type validation
29
+
30
+ ## How to use it?
31
+
32
+ ### Setup
33
+
34
+ ```python
35
+ from configparser import ConfigParser
36
+ from pathlib import Path
37
+ from confkit import Config
38
+
39
+ parser = ConfigParser()
40
+ Config.set_parser(parser)
41
+ Config.set_file(Path("config.ini"))
42
+ ```
43
+
44
+ ### Basic Usage
45
+
46
+ - Note: imports have been left out. see [examples/basic.py](basic.py) for the entire example.
47
+
48
+ ```python
49
+ class AppConfig:
50
+ debug = Config(False)
51
+ port = Config(8080)
52
+ host = Config("localhost")
53
+ timeout = Config(30.5)
54
+ api_key = Config("", optional=True)
55
+
56
+ config = AppConfig()
57
+ print(config.debug) # False
58
+ config.port = 9000 # Automatically saves to config.ini if write_on_edit is true (default).
59
+ ```
60
+
61
+ ### Enums and Custom Types
62
+
63
+ - Note: imports have been left out. see [examples/enums.py](enums.py) for the entire example.
64
+
65
+ ```python
66
+ class LogLevel(StrEnum):
67
+ DEBUG = "debug"
68
+ INFO = "info"
69
+ ERROR = "error"
70
+
71
+ class ServerConfig:
72
+ log_level = Config(ConfigEnum(LogLevel.INFO))
73
+ db_url = Config(String("sqlite:///app.db"))
74
+ fallback_level = Config(Optional(ConfigEnum(LogLevel.ERROR)))
75
+
76
+ config = ServerConfig()
77
+ config.log_level = LogLevel.DEBUG # Type-safe
78
+ ```
79
+
80
+ ### Method Decorators
81
+
82
+ - Note: imports have been left out. see [examples/decorators.py](decorators.py) for the entire example.
83
+
84
+ ```python
85
+ class ServiceConfig:
86
+ retry_count = Config(3)
87
+ timeout = Config(30)
88
+
89
+ @Config.with_setting(retry_count)
90
+ def process(self, data, **kwargs):
91
+ retries = kwargs.get('retry_count')
92
+ return f"Processing with {retries} retries"
93
+
94
+ @Config.as_kwarg("ServiceConfig", "timeout", "request_timeout", 60)
95
+ def request(self, url, **kwargs):
96
+ timeout = kwargs.get('request_timeout')
97
+ return f"Request timeout: {timeout}s"
98
+
99
+ service = ServiceConfig()
100
+ result = service.process("data") # Uses current retry_count
101
+ ```
102
+
103
+ ### Configuration File
104
+
105
+ Generated INI file structure see [examples/config.ini](config.ini) for the entire example.:
106
+
107
+ ```ini
108
+ [AppConfig]
109
+ debug = False
110
+ port = 9000
111
+ host = localhost
112
+ timeout = 30.5
113
+ api_key =
114
+
115
+ [ServiceConfig]
116
+ retry_count = 3
117
+ timeout = 30
118
+
119
+ [ServerConfig]
120
+ log_level = debug
121
+ db_url = sqlite:///app.db
122
+ fallback_level = error
123
+ ```
124
+
125
+ ## How to contribute?
126
+
127
+ 1. Fork the repository and clone locally
128
+ 2. Install dependencies: `uv sync --group test`
129
+ 3. Run tests: `pytest .`
130
+ 4. Run linting: `ruff check .`
131
+ 5. Make changes following existing patterns
132
+ 6. Add tests for new functionality
133
+ 7. Submit a pull request
134
+
135
+ ### Development
136
+
137
+ ```bash
138
+ git clone https://github.com/HEROgold/confkit.git
139
+ cd confkit
140
+ uv sync --group test
141
+ pytest .
142
+ ruff check .
143
+ ```
@@ -0,0 +1,9 @@
1
+ confkit/__init__.py,sha256=H4eKKhRm0joSURblV_hN5JFBJkeUsshnN-tiifuNnlA,679
2
+ confkit/config.py,sha256=WiXYbsew1APIFYBYN3YhWMTgYO3yh1m1FV_6tTy9zdM,13230
3
+ confkit/data_types.py,sha256=CQepO7F-pTa169rZBHAqfrdwHX8rs3ggGGgRUXyESIg,5026
4
+ confkit/exceptions.py,sha256=BSDW9I-DzHDBnb_uCL_p5tsqa3cR0ULNI4VybnUv_CU,274
5
+ confkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ confkit/sentinels.py,sha256=qiqeYxW7Mavhedd9RaVHqNn71vok-x9PJBlbYxnmcRY,372
7
+ confkit-0.1.1.dist-info/METADATA,sha256=2n2ro_5bf5JJPHFkVjkMA9egmscMUSgVJf7vOkbpXp8,3640
8
+ confkit-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ confkit-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any