0.1.5
Changed
- The rate-limiting documentation now describes Forward's actual limiter, read
from the server source rather than inferred from a plugin's constant. Three
corrections matter. The budget is per authenticated user per minute, not
per account, per host or per client, so every process using one set of
credentials spends from one allowance. Exceeding it does not slow a request,
it blocks the user for a lockout of one to sixty minutes, answered with 429
and aRetry-Aftercarrying the remaining block. And the ceiling is an org
setting defaulting to 2000 and adjustable from 1 to 10,000, disabled entirely
by default on self-hosted deployments, none of which the SDK can query. The
"auto"default of 1800 is unchanged but is now described as headroom
against a default rather than as a reading of your ceiling.
Added
-
to_api()onRepositoryQueryandDraftChange. These are frozen
dataclasses rather than pydantic models, so they never hadmodel_dump, and a
consumer calling it saw their query index come back empty instead of failing:
their normalisation swallowed theAttributeError, and an empty library is a
plausible answer, so the symptom surfaced two calls later. The method is named
to matchForwardModel.to_apiso serializing something the SDK returned does
not require knowing which of the two kinds it is.Note also that
forward_sdk._generated.modelsdeclares its own
RepositoryQuery, the wire schema these are built from. That one is a
pydantic model and is not what the library methods return. The docstrings now
say so. -
ForwardResponseError, so a response the SDK cannot parse stays inside the
exception tree. A body that failed validation previously raised pydantic's
ValidationError, which is not aForwardErrorand therefore travelled
straight through every consumer's error handling: a sync died with a
validation traceback in a job log rather than a failure it could classify and
report. Reported by the NetBox integration, which hit it on aNetwork
missingorgId.It carries the unparsed
payloadand themodel_namethe SDK tried to build,
and keeps the originalValidationErroras__cause__for per-field detail.
The wrapping is done once on the model base rather than at the 118 call sites
that parse a response, because a caller'sexcept ForwardErrorhas to cover
every one of them or it covers none.Required fields are unchanged and stay strict. They come from Forward's own
generated description, and a model with a hole in it would carry a wrong
response silently into whatever the caller writes next. The exposure is also
smaller than it looks: 170 of 276 models require at least one field, 379 in
total, andSnapshotInfo,Device,NqeRunResultandApiVersionrequire
none. The live suite passes unchanged against fwd.app, so real traffic carries
what the description promises on every path it covers. -
snapshot_cache_ttl, an opt-in cache for snapshot resolution, off by default.
Coverslatest_processedandlatest_collected_id, keyed by the exact
question so two tag scopes stay two answers, and cleared when an upload
through the same client makes it wrong.client.snapshots.clear_cache()
handles what the SDK cannot see.It exists for the rate limit rather than for latency. Forward's budget is per
authenticated user and exceeding it blocks the user, so a sync resolving a
snapshot once per slice across a thread pool spends a large share of an
allowance on a question with one answer. It stays off by default because a
stale snapshot does not raise, it returns real data from the wrong moment, and
only the caller knows how long "latest" should stay true. -
.github/workflows/live.ymlruns the live tests weekly against a configured
instance and opens an issue on failure. The unpublished endpoints have no
generated description to check against, so nothing in CI could notice Forward
changing one of their shapes; every such defect so far was reported by a
consumer whose sync broke. -
Six live tests under
TestShapesWithNoCiBackstop, each pinning a shape that
has been wrong at least once: rows surviving unrewritten, one page agreeing
with all rows, committed source reaching only from a concrete commit, source
without a path being refused before it is sent, a query carrying its commit,
and the current user parsing.
Published through the release workflow with PyPI Trusted Publishing, so this is the first release whose files carry attestations.