Caching#
In decorator mode, caching is enabled by default:
"""Caching — decorator mode with cache enabled."""
import os
from dataclasses import dataclass
import dature
os.environ["CACHE_HOST"] = "localhost"
os.environ["CACHE_PORT"] = "6379"
@dature.load(dature.EnvSource(prefix="CACHE_"), cache=True)
@dataclass
class CachedConfig:
host: str
port: int
config1 = CachedConfig()
os.environ["CACHE_PORT"] = "9999"
config2 = CachedConfig()
assert config1.port == 6379
assert config2.port == 6379
"""Caching — decorator mode with cache disabled."""
import os
from dataclasses import dataclass
import dature
os.environ["NOCACHE_HOST"] = "localhost"
os.environ["NOCACHE_PORT"] = "6379"
@dature.load(dature.EnvSource(prefix="NOCACHE_"), cache=False)
@dataclass
class UncachedConfig:
host: str
port: int
config3 = UncachedConfig()
os.environ["NOCACHE_PORT"] = "9999"
config4 = UncachedConfig()
assert config3.port == 6379
assert config4.port == 9999
"""Caching — TTL via timedelta (decorator mode)."""
import os
import time
from dataclasses import dataclass
from datetime import timedelta
import dature
os.environ["TTL_HOST"] = "localhost"
os.environ["TTL_PORT"] = "6379"
@dature.load(dature.EnvSource(prefix="TTL_"), cache=timedelta(seconds=30))
@dataclass
class TtlConfig:
host: str
port: int
config1 = TtlConfig()
os.environ["TTL_PORT"] = "9999"
config2 = TtlConfig()
assert config1.port == 6379
assert config2.port == 6379
# Simulate TTL expiration by monkey-patching the monotonic clock used internally
real_monotonic = time.monotonic
time.monotonic = lambda: real_monotonic() + 60.0
config3 = TtlConfig()
time.monotonic = real_monotonic
assert config3.port == 9999
Caching can also be configured globally via configure().
TTL caching#
cache accepts a datetime.timedelta in addition to bool:
cache=True— cache forevercache=False— never cachecache=timedelta(seconds=N)— cache for up toNseconds, then reload on the next accesscache=timedelta(0)— equivalent to "always miss" (reload on every access)
TTL is measured via time.monotonic(), so it is immune to system clock changes. A negative timedelta raises ValueError.
Bucket-aligned invalidation#
TTL is bucket-aligned (cron-style): the stored timestamp snaps down to the nearest monotonic % period == 0 boundary. The practical effect is that every class loaded inside the same TTL window invalidates at the same instant, regardless of when each individual load() happened.
Example with cache=timedelta(minutes=15):
| Moment | Action | Effect |
|---|---|---|
T=0 | Class A is first loaded | both A and B will invalidate at T=15 |
T=5 | Class B is first loaded | shares A's bucket → invalidates at T=15 |
T=15 | TTL boundary crossed | A and B go stale together |
T=16 | Class A reloaded | both refresh into the next bucket, expiring at T=30 |
The first load in a window has an effectively shortened TTL (up to one period less than the full duration). This is the standard cron-style trade-off and matches the intuitive "invalidate every N minutes" mental model.
Function-mode caching: Loader#
dature.load(src, schema=Cls) is a thin shortcut that constructs a throwaway Loader and calls .load() once. Repeated load(...) calls do not share a cache — each call is a fresh load.
To cache across calls in function mode, construct a Loader explicitly and keep the instance around:
"""Function-mode caching via an explicit ``Loader``.
``dature.load(...)`` is a thin shortcut that creates a throwaway ``Loader`` and
calls ``.load()`` once — repeated calls do NOT share a cache. To make caching
useful in function mode, keep the ``Loader`` instance around and call
``.load()`` multiple times.
"""
import os
from dataclasses import dataclass
from datetime import timedelta
import dature
os.environ["FN_HOST"] = "localhost"
os.environ["FN_PORT"] = "6379"
@dataclass
class FunctionConfig:
host: str
port: int
loader = dature.Loader(
dature.EnvSource(prefix="FN_"),
schema=FunctionConfig,
cache=timedelta(seconds=30),
)
first = loader.load()
os.environ["FN_PORT"] = "9999"
second = loader.load()
assert first.port == 6379
assert second.port == 6379 # cache still fresh — same Loader instance
The Loader carries all the load-time parameters and the cache state. Identity of the Loader instance fully captures the call configuration — there is no implicit fingerprinting of debug/type_loaders/strategy/etc. Different parameters → different Loader instances → independent cache slots.
Loader API#
| Method | Effect |
|---|---|
Loader.load() -> T | Return cached result if fresh, else reload and cache. |
Loader.invalidate() | Drop the cached result so the next .load() reloads from sources. |
Loader supports the same constructor parameters as dature.load(...) for function mode.