Migration
One note per minor release, listing only what you have to change. Each entry names the symptom first, so you can match what you are seeing rather than read the whole page.
This page covers 0.30 onward. For an older version, read the Breaking
sections in the changelog, newest first.
In 0.x the minor is the breaking position: a 0.x.0 release may change the
public API, and a 0.x.y release is a safe patch. So an upgrade within one
minor rarely needs this page. The one exception so far is
0.32.2, which changes no API but does change
what an already-open circuit does once.
Find your symptom
| Symptom | Release | Fix |
|---|---|---|
AttributeError: 'Operation' object has no attribute 'response' |
0.30 | Use result() |
A password or URL reads back as SecretStr(...) or *** |
0.31, 0.32 | Call .get_secret_value() |
| Metrics stopped arriving after upgrading, with no error | 0.31 | Set an endpoint |
TypeError on a SQLite adapter, either unexpected keyword argument 'path' or takes 1 positional argument |
0.32 | Pass provider= |
SettingsValidationError where you caught CoordinationSettingsValidationError |
0.32 | Catch the base error |
| An open circuit closed once, just after upgrading | 0.32.2 | Expected, happens once |
0.32
SQLite adapters take a provider, not a path
SQLiteLockAdapter and SQLiteScheduleAdapter now take provider=, like
every other SQLite adapter.
# Before
SQLiteLockAdapter("app.db")
# After
SQLiteLockAdapter(provider=SQLiteProvider("app.db"))
Better still, pass the provider to the component and let it build both:
sqlite = SQLiteProvider("app.db")
micro = Grelmicro(uses=[Coordination(sqlite)])
That also shares one connection across every component on the same file, where the old form opened its own.
A missing path now raises SettingsValidationError instead of
CoordinationSettingsValidationError. If you catch the latter, catch the
base error.
URL and header fields hide their credentials
url on PostgresConfig and RedisConfig, and endpoint on TraceConfig
and MetricsConfig, are now SecretUrl. Each headers value on
TraceConfig and MetricsConfig is now a SecretStr.
Passing a plain string still works. Only reading the value back changes:
# Before
dsn = config.url
# After
dsn = config.url.get_secret_value()
Nothing changes on the wire. The point is that repr(), model_dump() and
a ValidationError no longer carry the password.
0.31
Credential fields hide their value
basic_auth_password on TraceConfig and MetricsConfig, and password on
PostgresConfig and RedisConfig, are now SecretStr. Same shape as the
0.32 change above: passing a plain string still works, reading it back needs
.get_secret_value().
Metrics() no longer defaults to localhost
Metrics() now defaults to the auto exporter. With an endpoint configured
it exports over OTLP HTTP. Without one it auto-disables into a true no-op,
where it previously fell back to localhost:4318.
This one fails quietly. If you relied on the implicit localhost default, metrics simply stop arriving and nothing raises. Set the endpoint explicitly:
Metrics(endpoint="http://localhost:4318")
Or from the environment, GREL_METRICS_ENDPOINT.
The upside is that you can now register Metrics() unconditionally: an
auto-disabled Metrics installs no provider and never conflicts with a
second app.
0.30
Operation.response became result()
The idempotency Operation.response attribute is now a result() method,
typed as the stored type so a replay branch returns it without a cast.
async with idem(key) as op:
if op.replayed:
return op.result() # was: op.response
...
It is valid only on a replay. Calling it on a first execution raises
IdempotencyStateError, so keep it behind if op.replayed:.
0.32.2, not breaking but worth knowing
Circuit breakers now reclaim their stored state instead of keeping it
forever. Rows written before 0.32.2 carry no activity timestamp, so an
already-open circuit on Postgres or SQLite reads as expired the first time
the new code touches it and starts again from CLOSED.
This happens once, on the first call after the upgrade. Circuits held open
by isolate() are unaffected.
About deprecation
Before 1.0 a rename is a clean cut: the old name is removed in the same
release the new one appears, with no deprecation cycle and no alias. That
keeps a fast-moving 0.x from accumulating shims, and it is why this page
exists instead.
After 1.0, 1.x follows standard semver and breaking changes go through a
deprecation cycle.