Skip to content

Commit e20e5ef

Browse files
authored
Add YAML file helpers and mixed-format configuration loading (#4)
* feat: add config handler utilities for TOML-based configuration loading This commit introduces a new ConfigHandler API and a load_config helper for loading and merging multiple TOML configuration files. Included in this update: - add ConfigHandler with nested attribute access and dot-path lookups - add support for merging multiple TOML config files into one configuration object - export the new config helpers from the public package interface - make TOML writing handle missing tomli_w imports more safely - add tests for merged config loading and access patterns - update the README and bump the package version to 1.3.1 * docs: expand load_config documentation and refine public API This commit improves the documentation for load_config and cleans up how the configuration helper is exposed in the public interface. Included in this update: - document load_config in the README, quickstart guide, and tools index - add a dedicated load_config tool documentation page with examples and behavior notes - remove ConfigHandler from the top-level public exports to keep the API more focused - add docstrings to the config handler implementation for clarity and maintainability - update the related tests to reflect the refined public usage * test: reorganize static tests and expand config-based IO coverage This commit restructures the static test setup and extends the test runner to cover split TOML configuration loading and merged config access. Included in this update: - move the static test module from tmp_tests to static_tests - add a dedicated static test package structure - expand the static IO test runner to write and merge multiple TOML files - add coverage for nested config access and merged configuration values - keep the JSON read and write flow as part of the static integration-style test run * feat: add YAML IO support and extend mixed-format config loading This commit introduces YAML read and write helpers, expands load_config to support TOML, JSON, and YAML inputs, and updates the docs and tests accordingly. Included in this update: - add read_yaml and write_yaml file helpers - add YAML-specific IO exceptions and the PyYAML dependency - extend load_config to merge TOML, JSON, YAML, and YML files in order - validate that loaded config roots are mappings before merging - update the README, quickstart guide, and tool docs for YAML and mixed-format config loading - replace the old static test runner with focused tool-level tests for config loading and YAML IO
1 parent 3a30c8c commit e20e5ef

17 files changed

Lines changed: 595 additions & 115 deletions

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
## What You Get
66

77
- simple text and binary file helpers
8-
- JSON and TOML read/write helpers
8+
- JSON, TOML, and YAML read/write helpers
99
- masking for tokens, secrets, IDs, and similar values
1010
- global configuration for shared error behavior
1111
- ready-to-use console and file logging
@@ -37,6 +37,8 @@ loaded = read_json("tmp/config.json")
3737
logger.info("Loaded config: %s", loaded)
3838
```
3939

40+
For split configuration setups, `load_config()` merges multiple TOML, JSON, or YAML files into one config object with dot access.
41+
4042
## Public API Overview
4143

4244
### Core
@@ -53,6 +55,9 @@ logger.info("Loaded config: %s", loaded)
5355
- `write_json`
5456
- `read_toml`
5557
- `write_toml`
58+
- `read_yaml`
59+
- `write_yaml`
60+
- `load_config`
5661

5762
### Logging helpers
5863

docs/quickstart.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ loaded = read_json("tmp/config.json")
2626
logger.info("Loaded config: %s", loaded)
2727
```
2828

29+
If your configuration is split across multiple TOML, JSON, or YAML files, `load_config()` merges it into one object:
30+
31+
```python
32+
from clevertools import load_config
33+
34+
config = load_config("config/settings.toml", "config/content.yaml")
35+
print(config.pipelines.ai.enabled)
36+
```
37+
2938
Next steps:
3039

3140
- Read [Tools](./tools/README.md) for every public helper.

docs/tools/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Each public helper has its own page with signature, behavior, and examples.
1616
- [write_json](./write_json.md)
1717
- [read_toml](./read_toml.md)
1818
- [write_toml](./write_toml.md)
19+
- [read_yaml](./read_yaml.md)
20+
- [write_yaml](./write_yaml.md)
21+
- [load_config](./load_config.md)
1922

2023
## Logging helpers
2124

docs/tools/load_config.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# `load_config`
2+
3+
`load_config()` reads multiple configuration files, merges overlapping sections, and returns one combined configuration object.
4+
5+
## Signature
6+
7+
```python
8+
load_config(*file_paths: str | Path, on_error: ErrorMode | None = None)
9+
```
10+
11+
## What it does
12+
13+
- Reads each file in the order you pass it in.
14+
- Supports `.toml`, `.json`, `.yaml`, and `.yml`.
15+
- Merges nested tables recursively.
16+
- Combines shared sections such as `pipelines.ai` across multiple files.
17+
- Exposes the merged result through attribute access and dot-path lookups.
18+
- Applies the shared error policy when a file cannot be read or parsed.
19+
20+
## Returns
21+
22+
Returns one merged configuration object.
23+
24+
You can access values like this:
25+
26+
- `config.pipelines.ai.enabled`
27+
- `config.get("pipelines.ai.ai_model")`
28+
29+
## Example
30+
31+
```python
32+
from clevertools import load_config
33+
34+
config = load_config(
35+
"config/settings.toml",
36+
"config/content.json",
37+
"config/content.yaml",
38+
)
39+
40+
print(config.pipelines.ai.enabled)
41+
print(config.pipelines.ai.ai_model)
42+
print(config.get("pipelines.publishing.default_post_status"))
43+
```
44+
45+
## Notes
46+
47+
- Nested mappings are merged recursively across supported file types.
48+
- If the same non-mapping key exists in multiple files, the later file wins.
49+
- Missing files and parse errors follow the shared error policy from the corresponding reader.
50+
- Each loaded document must have a mapping object at its root so it can be merged safely.
51+
- Use `as_dict()` when you need the merged result as a plain dictionary.

docs/tools/read_yaml.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# `read_yaml`
2+
3+
`read_yaml()` reads a YAML file and deserializes it with `yaml.safe_load()`.
4+
5+
## Signature
6+
7+
```python
8+
read_yaml(
9+
file_path: str | Path,
10+
on_error: Literal["raise", "log", "silent"] | None = None,
11+
) -> Any | None
12+
```
13+
14+
## Example
15+
16+
```python
17+
from clevertools import read_yaml
18+
19+
config = read_yaml("config.yaml")
20+
print(config["service"])
21+
```
22+
23+
## Notes
24+
25+
- Invalid YAML returns `None` unless the active error mode raises.
26+
- Missing files and directory paths are handled through the shared error policy.

docs/tools/write_yaml.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# `write_yaml`
2+
3+
`write_yaml()` serializes a Python value to YAML and writes it to disk.
4+
5+
## Signature
6+
7+
```python
8+
write_yaml(
9+
file_path: str | Path,
10+
data: Any,
11+
create_if_missing: bool = True,
12+
allow_unicode: bool = True,
13+
sort_keys: bool = False,
14+
on_error: Literal["raise", "log", "silent"] | None = None,
15+
) -> None
16+
```
17+
18+
## Example
19+
20+
```python
21+
from clevertools import write_yaml
22+
23+
write_yaml(
24+
"config.yaml",
25+
{
26+
"service": "clevertools",
27+
"enabled": True,
28+
"labels": ["yaml", "config"],
29+
},
30+
allow_unicode=True,
31+
sort_keys=False,
32+
)
33+
```
34+
35+
## Notes
36+
37+
- `data` must not be `None`.
38+
- With `create_if_missing=True`, parent folders are created automatically.
39+
- Serialization errors are handled through the shared error policy.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "clevertools"
7-
version = "1.2.1"
7+
version = "1.3.1"
88
description = "Clevertools is a utility library providing practical tools for common workflows."
99
readme = "README.md"
1010
requires-python = ">=3.11"

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ mypy
55
faker
66

77
# === For tools to work === #
8-
tomli_w
8+
tomli_w
9+
pyyaml

src/clevertools/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from .configuration import configure
4+
from .system.config_handler import load_config
45

56
from .errors.exceptions import * # noqa: F403
67

@@ -13,6 +14,7 @@
1314
from .file.default_io import read, write
1415
from .file.json_io import read_json, write_json
1516
from .file.toml_io import read_toml, write_toml
17+
from .file.yaml_io import read_yaml, write_yaml
1618

1719
from .system.mask_handler import mask
1820

@@ -30,6 +32,8 @@
3032
"write_json",
3133
"read_toml",
3234
"write_toml",
35+
"read_yaml",
36+
"write_yaml",
3337
"configure_logger",
3438
"get_logger",
3539
"CleverToolsFormatter",
@@ -38,6 +42,7 @@
3842
"reset_handlers",
3943
"resolve_logger_options",
4044
"configure",
45+
"load_config",
4146

4247
# === Exceptions === #
4348
"AppError",

src/clevertools/errors/exceptions.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1110,9 +1110,22 @@ class InternalProtocolError(ExecutionError):
11101110

11111111
class ExternalProtocolMismatchError(ProtocolError):
11121112
"""Raised when external protocol versions or formats mismatch."""
1113-
1113+
1114+
class YamlIOError(Exception):
1115+
"""Base exception for YAML IO operations."""
1116+
1117+
1118+
class YamlReadError(YamlIOError):
1119+
"""Raised when reading YAML fails."""
1120+
1121+
1122+
class YamlWriteError(YamlIOError):
1123+
"""Raised when writing YAML fails."""
11141124

11151125
__all__ = (
1126+
"YamlIOError",
1127+
"YamlReadError",
1128+
"YamlWriteError",
11161129
"AppError",
11171130
"RecoverableError",
11181131
"FatalError",

0 commit comments

Comments
 (0)