Skip to content

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.