Live config subscriptions via ConfigWatcher and WatchedField[T].
Create a watcher from a client. Register fields before entering the context manager:
from opendecree import ConfigClient
with ConfigClient("localhost:9090", subject="myapp") as client:
with client.watch("tenant-id") as watcher:
fee = watcher.field("payments.fee", float, default=0.01)
enabled = watcher.field("payments.enabled", bool, default=False)
# Read current values
print(fee.value) # 0.025 (float, always fresh)
print(enabled.value) # True (bool)The watcher:
- Loads the current config snapshot on enter
- Subscribes to changes via gRPC server-streaming
- Updates field values atomically in the background
- Auto-reconnects with exponential backoff on connection loss
- Stops the background thread on exit
Each registered field returns a WatchedField[T] with:
fee = watcher.field("payments.fee", float, default=0.01)
print(fee.value) # always the latest value, thread-safeenabled = watcher.field("payments.enabled", bool, default=False)
if enabled: # uses __bool__, checks the live value
print("Feature is enabled")Falsy values: False, 0, 0.0, "", None.
@fee.on_change
def handle_fee_change(old: float, new: float):
print(f"Fee changed: {old} -> {new}")Callbacks run on the watcher's background thread. Keep them fast — slow callbacks block other field updates.
for change in fee.changes():
print(f"{change.field_path}: {change.old_value} -> {change.new_value}")The iterator blocks until a change arrives. It stops when the watcher exits.
| Type | Example | Default suggestion |
|---|---|---|
str |
"hello" |
"" |
int |
42 |
0 |
float |
3.14 |
0.0 |
bool |
True |
False |
timedelta |
timedelta(seconds=30) |
timedelta() |
Register fields before the with block. Fields cannot be added after the watcher starts:
watcher = client.watch("tenant-id")
# Register fields first
fee = watcher.field("payments.fee", float, default=0.01)
# Then start
with watcher:
print(fee.value)Or equivalently:
with client.watch("tenant-id") as watcher:
fee = watcher.field("payments.fee", float, default=0.01)
# ...Wait — fields must be registered before start(). When using the two-line form, register between watch() and with. When using the one-line form, the watcher loads a snapshot on enter, so fields registered inside the with block will get their initial values from the snapshot.
If the gRPC stream drops (server restart, network issue), the watcher automatically reconnects with exponential backoff:
- Initial delay: 1 second
- Maximum delay: 30 seconds
- Multiplier: 2x
- Jitter: 0.5x–1.5x
During reconnection, field.value returns the last known value. No action needed from your code.
You can create multiple watchers for different tenants:
with ConfigClient("localhost:9090", subject="myapp") as client:
with client.watch("tenant-a") as watcher_a:
with client.watch("tenant-b") as watcher_b:
fee_a = watcher_a.field("payments.fee", float, default=0.01)
fee_b = watcher_b.field("payments.fee", float, default=0.01)
# Both update independently- Async Usage — async watcher with
async foriteration - Configuration — client options (auth, TLS, retry)