Skip to content

Commit d7a3779

Browse files
committed
Add json yaml toml supports
1 parent eef3d3a commit d7a3779

38 files changed

+2835
-762
lines changed

CHANGELOG.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,58 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
- **File-Based Sources**: New YAML, JSON, and TOML sources for loading configuration from files
12+
- `sources.YAML(file_path, ...)` - Load configuration from YAML files
13+
- `sources.JSON(file_path, ...)` - Load configuration from JSON files
14+
- `sources.TOML(file_path, ...)` - Load configuration from TOML files
15+
- All file sources support nested configuration structures (automatically flattened to dot notation)
16+
- All file sources support `required=False` parameter for graceful handling of missing files
17+
- Missing files return empty dict and show "Not Available" status in `--check-variables`
18+
- **Source ID System**: Enhanced source identification for multiple sources of the same type
19+
- Each source now has a unique `id` property (in addition to `name`)
20+
- Custom source IDs can be specified via `source_id` parameter
21+
- Automatic ID generation based on source type and key parameters
22+
- `PriorityPolicy` now supports both source names and source IDs
23+
- Multiple sources of the same type can be used with different priorities
24+
- **Enhanced Diagnostic Table**: Improved `--check-variables` output
25+
- Added "Status" column showing source load status ("Active", "Not Available", "Failed: ...")
26+
- Better error messages for missing files (shows "Not Available" instead of "Error")
27+
- Source status tracking via `load_status` and `load_error` properties
28+
- **Improved Error Messages**: Required field errors now include field descriptions from metadata
29+
- `RequiredFieldError` messages now show field descriptions when available
30+
- More user-friendly error messages with actionable guidance
31+
32+
### Changed
33+
- **Dependencies**: Moved `python-dotenv`, `pyyaml`, and `tomli` from optional to core dependencies
34+
- `dotenv`, `yaml`, and `toml` sources are now always available
35+
- Only `etcd` remains as an optional dependency
36+
- **Etcd Source**: Removed `Etcd.from_env()` method
37+
- All parameters must now be passed explicitly via `__init__`
38+
- Users should read environment variables themselves and pass to `Etcd()`
39+
- This aligns with the principle that the library should not implicitly read environment variables for its own configuration
40+
- **Source Base Class**: Enhanced `Source` base class with status tracking
41+
- Added `_load_status` attribute ("success", "not_found", "failed", "unknown")
42+
- Added `_load_error` attribute for error messages
43+
- Added `load_status` and `load_error` properties
44+
- Modified `load()` to wrap `_do_load()` with proper error handling
45+
- Subclasses should implement `_do_load()` instead of `load()`
46+
- **Key Mapping**: File-based sources (YAML, JSON, TOML) use recursive flattening
47+
- Nested dictionaries are automatically flattened to dot notation (e.g., `{"db": {"host": "localhost"}}``{"db.host": "localhost"}`)
48+
- Consistent with existing key mapping rules (`__``.`, `_` preserved, lowercase)
49+
- **Examples**: Updated all examples to use best practices
50+
- Use nested dataclasses instead of double-underscore fields
51+
- Include field descriptions in metadata
52+
- Proper error handling with `RequiredFieldError`
53+
- Support for `--help` and `-cv` flags
54+
55+
### Fixed
56+
- Fixed "Unknown" status in diagnostic table for Env, CLI, Defaults, and DotEnv sources
57+
- Fixed source status tracking to correctly show "Active", "Not Available", or "Failed" status
58+
- Improved test coverage for file-based sources and multiple sources of the same type
59+
860
## [0.5.0] - 2026-01-07
961

1062
### Fixed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
File Sources Example
2+
====================
3+
4+
Example demonstrating YAML, JSON, and TOML file sources.
5+
6+
.. literalinclude:: ../../../examples/file_sources_example.py
7+
:language: python
8+
:linenos:
9+

docs/source/examples/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ Real-world usage examples.
1111
validation_example
1212
nested_validation_example
1313
logging_example
14+
file_sources_example
1415
etcd_example
1516

docs/source/user_guide/etcd.rst

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,33 @@ Create an Etcd source directly:
5353
From Environment Variables (Recommended)
5454
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5555

56-
The recommended approach is to use ``Etcd.from_env()`` to read configuration from environment variables. This solves the "bootstrap problem" of how to configure the etcd source itself.
56+
The recommended approach is to read environment variables yourself and pass them to ``Etcd()``. This aligns with the principle that the library should not implicitly read environment variables for its own configuration.
5757

5858
.. code-block:: python
5959
6060
from varlord import Config
6161
from varlord.sources import Etcd
6262
from dataclasses import dataclass, field
63+
import os
6364
6465
@dataclass
6566
class AppConfig:
6667
host: str = field()
6768
port: int = field(default=8000)
6869
debug: bool = field(default=False)
6970
70-
# From environment variables
71-
etcd_source = Etcd.from_env(prefix="/app/")
71+
# Read environment variables and pass to Etcd
72+
etcd_source = Etcd(
73+
host=os.environ.get("ETCD_HOST", "127.0.0.1"),
74+
port=int(os.environ.get("ETCD_PORT", "2379")),
75+
prefix=os.environ.get("ETCD_PREFIX", "/app/"),
76+
ca_cert=os.environ.get("ETCD_CA_CERT"),
77+
cert_key=os.environ.get("ETCD_CERT_KEY"),
78+
cert_cert=os.environ.get("ETCD_CERT_CERT"),
79+
user=os.environ.get("ETCD_USER"),
80+
password=os.environ.get("ETCD_PASSWORD"),
81+
watch=os.environ.get("ETCD_WATCH", "").lower() in ("true", "1", "yes", "on"),
82+
)
7283
7384
cfg = Config(
7485
model=AppConfig,
@@ -77,6 +88,8 @@ The recommended approach is to use ``Etcd.from_env()`` to read configuration fro
7788
7889
app = cfg.load()
7990
91+
**Note**: The ``Etcd.from_env()`` method has been removed. All parameters must be passed explicitly via ``__init__``.
92+
8093
Environment Variables
8194
---------------------
8295

@@ -138,11 +151,20 @@ Then load it with python-dotenv:
138151
139152
load_dotenv() # Load .env file
140153
154+
import os
155+
156+
etcd_source = Etcd(
157+
host=os.environ.get("ETCD_HOST", "127.0.0.1"),
158+
port=int(os.environ.get("ETCD_PORT", "2379")),
159+
prefix=os.environ.get("ETCD_PREFIX", "/app/"),
160+
ca_cert=os.environ.get("ETCD_CA_CERT"),
161+
cert_key=os.environ.get("ETCD_CERT_KEY"),
162+
cert_cert=os.environ.get("ETCD_CERT_CERT"),
163+
)
164+
141165
cfg = Config(
142166
model=AppConfig,
143-
sources=[
144-
Etcd.from_env(), # Read from environment variables
145-
],
167+
sources=[etcd_source],
146168
)
147169
148170
TLS Configuration
@@ -257,7 +279,15 @@ Enable watch support for dynamic configuration updates. Etcd source can watch fo
257279
cfg = Config(
258280
model=AppConfig,
259281
sources=[
260-
Etcd.from_env(prefix="/app/", watch=True),
282+
Etcd(
283+
host=os.environ.get("ETCD_HOST", "127.0.0.1"),
284+
port=int(os.environ.get("ETCD_PORT", "2379")),
285+
prefix="/app/",
286+
watch=True,
287+
ca_cert=os.environ.get("ETCD_CA_CERT"),
288+
cert_key=os.environ.get("ETCD_CERT_KEY"),
289+
cert_cert=os.environ.get("ETCD_CERT_CERT"),
290+
),
261291
],
262292
)
263293
@@ -341,7 +371,15 @@ Watch events include:
341371
cfg = Config(
342372
model=AppConfig,
343373
sources=[
344-
Etcd.from_env(prefix="/app/", watch=True),
374+
Etcd(
375+
host=os.environ.get("ETCD_HOST", "127.0.0.1"),
376+
port=int(os.environ.get("ETCD_PORT", "2379")),
377+
prefix="/app/",
378+
watch=True,
379+
ca_cert=os.environ.get("ETCD_CA_CERT"),
380+
cert_key=os.environ.get("ETCD_CERT_KEY"),
381+
cert_cert=os.environ.get("ETCD_CERT_CERT"),
382+
),
345383
],
346384
)
347385
@@ -371,7 +409,15 @@ For automatic updates, use ``ConfigStore`` which handles watch automatically:
371409
cfg = Config(
372410
model=AppConfig,
373411
sources=[
374-
Etcd.from_env(prefix="/app/", watch=True),
412+
Etcd(
413+
host=os.environ.get("ETCD_HOST", "127.0.0.1"),
414+
port=int(os.environ.get("ETCD_PORT", "2379")),
415+
prefix="/app/",
416+
watch=True,
417+
ca_cert=os.environ.get("ETCD_CA_CERT"),
418+
cert_key=os.environ.get("ETCD_CERT_KEY"),
419+
cert_cert=os.environ.get("ETCD_CERT_CERT"),
420+
),
375421
],
376422
)
377423
@@ -402,7 +448,14 @@ Etcd source can be combined with other sources. Later sources override earlier o
402448
cfg = Config(
403449
model=AppConfig,
404450
sources=[
405-
Etcd.from_env(prefix="/app/"), # Load from etcd
451+
Etcd(
452+
host=os.environ.get("ETCD_HOST", "127.0.0.1"),
453+
port=int(os.environ.get("ETCD_PORT", "2379")),
454+
prefix="/app/",
455+
ca_cert=os.environ.get("ETCD_CA_CERT"),
456+
cert_key=os.environ.get("ETCD_CERT_KEY"),
457+
cert_cert=os.environ.get("ETCD_CERT_CERT"),
458+
), # Load from etcd
406459
Env(), # Env can override etcd
407460
CLI(), # CLI can override all
408461
],
@@ -478,7 +531,7 @@ Nested Configuration
478531
Best Practices
479532
--------------
480533

481-
1. **Use Environment Variables**: Use ``Etcd.from_env()`` instead of hardcoding connection parameters
534+
1. **Use Environment Variables**: Read environment variables yourself and pass them to ``Etcd()`` instead of hardcoding connection parameters
482535
2. **Use .env Files**: Manage configuration in development with ``.env`` files
483536
3. **Enable Watch**: Enable ``watch=True`` for configurations that need dynamic updates
484537
4. **Use Prefixes**: Use different etcd prefixes for different applications to avoid key conflicts

0 commit comments

Comments
 (0)