Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
8300e68
Add watch for generators
ogenstad Jun 25, 2026
10a45a0
ci: dispatch infrahub-sdk updates to infrahub-sync
Jul 2, 2026
7fce489
Add missing protocols for Infrahub 1.11 (#1134)
ogenstad Jul 3, 2026
9a85580
add MERGE_FAILED to the BranchStatus enum (#1123)
ajtmccarty Jul 8, 2026
169f1cb
add the "ordered" schema attribute property (#1150)
ajtmccarty Jul 9, 2026
08fa009
fix(ctl): import pyarrow lazily in the JSON importer
Jul 10, 2026
645553a
spec(ihs-249): SDK retry with backoff on HTTP 429 responses
dgarros Jul 7, 2026
97886fd
plan(ihs-249): design SDK 429 retry with backoff
dgarros Jul 7, 2026
f821605
critique(ihs-249): dual-lens review + apply must-address fixes
dgarros Jul 7, 2026
54d96aa
tasks(ihs-249): dependency-ordered task breakdown
dgarros Jul 7, 2026
51cd2c5
align(ihs-249): spec/PRD alignment check — ALIGNED
dgarros Jul 7, 2026
1fe0ccd
plan(ihs-249): make async+sync dual coverage explicit
dgarros Jul 7, 2026
26ec3ec
feat(client): add HTTP 429 retry with backoff (handler, config, drivers)
dgarros Jul 7, 2026
ef6bbc5
test: add client-level 429 retry tests for _request path (US1)
dgarros Jul 7, 2026
ce019bd
test(sdk): add US2 Retry-After client tests for 429 retry driver
dgarros Jul 7, 2026
112a7c5
test(rate-limit): client-level 429 retry exhaustion tests (US3)
dgarros Jul 7, 2026
28a33ad
test(sdk): add US4 disable/tune/parity 429 retry tests (T016)
dgarros Jul 7, 2026
d417166
test: cover all request paths and multipart body re-read for 429 retry
dgarros Jul 7, 2026
bd93619
fix: floor negative and guard overflow in Retry-After delta parsing
dgarros Jul 7, 2026
ab49028
docs(ihs-249): opsmill implement report — DONE
dgarros Jul 7, 2026
fdfbe23
ci: fix markdownlint and vale errors in ihs-249 spec docs
dgarros Jul 8, 2026
c40ab56
fix(client): harden 429 retry exhaustion, streaming cleanup, and backoff
dgarros Jul 9, 2026
d4125e1
chore: stop tracking .specify/feature.json
dgarros Jul 10, 2026
f1d46e3
refactor(client): encapsulate 429 retry in RateLimitRetryHandler; tri…
dgarros Jul 10, 2026
f828cb1
chore(changelog): drop 1124.changed.md, keep a single fragment
dgarros Jul 10, 2026
c401806
feat(ctl): add deployment_id to the info command
dgarros Jul 11, 2026
9c74bfa
chore(changelog): add fragment for deployment_id info feature
dgarros Jul 11, 2026
fefd01c
refactor(client): combine version and deployment_id into get_server_i…
dgarros Jul 12, 2026
6fe1bf1
feat(release): derive package version from git tags via hatch-vcs
fatih-acar Jul 10, 2026
07eb001
docs(specs): add spec for SDK X-Priority request header (IHS-259)
dgarros Jul 10, 2026
bb88b2d
docs(specs): add implementation plan for X-Priority header (IHS-259)
dgarros Jul 10, 2026
62e6be2
docs(specs): critique X-Priority spec/plan and apply refinements (IHS…
dgarros Jul 10, 2026
72540fd
docs(specs): add dependency-ordered tasks for X-Priority header (IHS-…
dgarros Jul 10, 2026
5a0176e
docs(specs): add spec/ask alignment check for X-Priority (IHS-259)
dgarros Jul 10, 2026
2506cb6
chore(specs): mark T001 setup done (baseline 107 passed) [IHS-259]
dgarros Jul 11, 2026
e9e85ca
feat(sdk): add Priority enum and Config.priority field [IHS-259]
dgarros Jul 11, 2026
40d7ae6
feat(sdk): emit X-Priority default header across transports [IHS-259]
dgarros Jul 11, 2026
3a490da
test(sdk): assert no X-Priority header when unconfigured [IHS-259]
dgarros Jul 11, 2026
fead632
feat(sdk): add per-request priority override across client + node [IH…
dgarros Jul 11, 2026
43990f3
test(sdk): validate Config.priority accepts case-insensitive, rejects…
dgarros Jul 11, 2026
a6b7d1d
test(sdk): assert async/sync parity via priority resolution truth tab…
dgarros Jul 11, 2026
89e848a
docs(sdk): docstrings, regenerated docs, and changelog for X-Priority…
dgarros Jul 11, 2026
f0338b8
fix(client): keep live auth header on relogin retry after priority me…
dgarros Jul 11, 2026
e0b4725
test(sdk): cover node delete/update and all diff methods for priority…
dgarros Jul 11, 2026
02b4da5
docs(specs): add opsmill implement report for X-Priority [IHS-259]
dgarros Jul 11, 2026
f736cc6
docs(sdk): regenerate SDK reference docs after rebase onto develop [I…
dgarros Jul 12, 2026
d3c2493
docs(specs): fix markdown lint in X-Priority critique doc [IHS-259]
dgarros Jul 12, 2026
3b964a8
chore: stop tracking .specify/feature.json [IHS-259]
dgarros Jul 12, 2026
7e99f52
feat(sdk): thread priority through count and resource-pool peer fetch…
dgarros Jul 12, 2026
f7eb7ea
docs(sdk): shrink changelog, fix contract/data-model, regenerate docs…
dgarros Jul 12, 2026
ef694f5
test(sdk): drop ty-ignore in Config.priority tests via dict[str, Any]…
dgarros Jul 12, 2026
cd5a2c3
refactor(client): layer per-request header deltas over base headers […
dgarros Jul 14, 2026
ab60207
chore(sdk): apply review nits — Priority import, docstrings, test cle…
dgarros Jul 14, 2026
48b9fd6
docs(sdk): regenerate SDK reference after rebase onto infrahub-develo…
dgarros Jul 14, 2026
2952d5a
feat(sdk): add task retry/cancel methods and expose available actions…
polmichel Jul 16, 2026
01f7c6f
chore(ruff): adopt google docstring convention, enforce DOC102/DOC402
dgarros Jul 15, 2026
7ee6566
chore(ruff): trim redundant comments around docstring convention config
dgarros Jul 16, 2026
fbcb569
feat(sdk): expose task error and webhook delivery diagnostics (#1197)
polmichel Jul 21, 2026
fd7f55b
feat: add IPAddress attribute kind support [INFP-551]
PhillSimonds Jul 19, 2026
3596555
perf(client): pass pagination offset and limit as GraphQL variables
Jul 23, 2026
816d834
feat(client): carry request priority on RequestContext
dgarros Jul 16, 2026
ba6f582
refactor(priority): rename the middle tier NORMAL to MEDIUM
dgarros Jul 16, 2026
b0b5bce
feat(config): raise default rate_limit_max_retries from 5 to 10
Jul 24, 2026
c9e8b5d
feat(schema): generate user-facing write/read schema models [INFP-234]
dgarros Jul 3, 2026
e281381
feat(schema): offline write-contract validation + generated-model dri…
dgarros Jul 3, 2026
c121d45
fix(schema): gate schema extensions through the write contract
dgarros Jul 3, 2026
632e542
refactor(schema): name generated models InfrahubSchema{Write,Read} + …
dgarros Jul 5, 2026
37f5847
refactor(schema): validate against InfrahubSchemaWrite root instead o…
dgarros Jul 5, 2026
cc72f69
feat(schema): generate a typed ComputedAttribute model instead of dic…
dgarros Jul 5, 2026
30ddc7f
feat(schema): emit typed models for choices, parameters, and computed…
dgarros Jul 5, 2026
0db7361
feat(schema): model generated attribute as a kind-discriminated union
dgarros Jul 5, 2026
f728151
feat(schema): add extensions to write model and profile/template read…
dgarros Jul 6, 2026
231a285
feat(schema): type generated SDK schema fields with dedicated enums
dgarros Jul 6, 2026
ea27bcf
refactor(schema): back public SDK schema models with generated models…
dgarros Jul 6, 2026
44d882c
fix(schema): render generated enum-typed defaults as enum members
dgarros Jul 6, 2026
9161e24
test(schema): suppress expected ty error for raw-string enum coercion…
dgarros Jul 7, 2026
ea11136
refactor(schema): move kind/hash onto generated read models [INFP-234]
dgarros Jul 7, 2026
b951dc7
feat(schema): expose kind as a property on write schema nodes [INFP-234]
dgarros Jul 8, 2026
b1dcb35
chore(schema): regenerate SDK models and protocols after develop reba…
dgarros Jul 14, 2026
12af188
fix(schema): normalize schema.load/check payloads to the write contra…
dgarros Jul 16, 2026
1face70
Revert "normalize schema.load/check payloads to the write contract" […
dgarros Jul 16, 2026
165ea52
refactor(schema): address PR review on SDK schema models and tests [I…
dgarros Jul 22, 2026
d70aaa1
refactor(schema): tolerate and drop extra fields on write/read models…
dgarros Jul 22, 2026
48359e9
docs(changelog): drop breaking label from SDK schema-models note [INF…
dgarros Jul 22, 2026
438f1cd
test(schema): skip IPAddress node tests pending backend attribute kin…
dgarros Jul 23, 2026
07bc7ba
fix(schema): require version on the generated write root [INFP-234]
dgarros Jul 24, 2026
7779004
chore(review): clarify schema model comments [INFP-234]
dgarros Jul 26, 2026
72fc224
feat(ctl): add `infrahubctl schema format` command
petercrocker Jul 19, 2026
c59cf87
refactor(ctl): use ruamel.yaml so schema format preserves comments
petercrocker Jul 19, 2026
d0c3a32
ci(ctl): warn-only check for Infrahub JSON schema drift
petercrocker Jul 20, 2026
75a5b2c
fix(ctl): harden schema formatter against malformed sections and head…
petercrocker Jul 20, 2026
1ea829b
style(ctl): add blank line after last docstring section (ruff D413)
petercrocker Jul 20, 2026
c0cac67
docs: regenerate infrahubctl schema reference after develop merge
petercrocker Jul 20, 2026
e6f65a3
fix(ctl): colour schema-format --diff via Rich style, not inline markup
petercrocker Jul 20, 2026
45f367e
feat(ctl): add opt-in strip-defaults / sort / backfill flags to schem…
petercrocker Jul 21, 2026
99e923a
refactor(ctl): return an enum from schema-format helper; harden sort …
petercrocker Jul 29, 2026
8f4de3c
test(ctl): cover schema-format error paths and drift fetch/baseline IO
petercrocker Jul 29, 2026
7d0c1b0
fix(ctl): treat unparseable schema files as a per-file format error
petercrocker Jul 29, 2026
4ea2bbf
feat(schema): add IPAddress to the generated attribute-kind models (#…
ajtmccarty Jul 31, 2026
78060c6
feat: report the schema fields the write contract does not apply
dgarros Jul 31, 2026
5fabbec
fix: name a nested read-only field relative to its owner
dgarros Jul 31, 2026
9474ffe
refactor: look the read-only table up by class name alone
dgarros Aug 2, 2026
a592f7c
Remove broken client.branch.diff_data() (#1229)
saltas888 Aug 11, 2026
22fc7ac
feat: warn when a watch block is missing or incomplete in .infrahub.y…
ogenstad Aug 18, 2026
c7313d5
add release notes
wvandeun Aug 19, 2026
eabb609
fix
wvandeun Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
uses: "actions/checkout@v7"
with:
submodules: true
# Full history + tags so hatch-vcs stamps the real tag version at build time, not the fallback
fetch-depth: 0
fetch-tags: true

- name: Cache UV dependencies
uses: "actions/cache@v6"
Expand Down
21 changes: 17 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
uses: "actions/checkout@v7"
with:
submodules: true
# Full history + tags so hatch-vcs resolves the exact tag version from installed metadata
fetch-depth: 0
fetch-tags: true

- name: "Set up Python"
uses: "actions/setup-python@v7"
Expand All @@ -41,10 +44,13 @@ jobs:
- name: Check prerelease type
id: release
run: |
VERSION=$(uv version --short)
VERSION=$(uv run python -c "import importlib.metadata; print(importlib.metadata.version('infrahub-sdk'))")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo is_prerelease=$(uv run python -c "from packaging.version import Version; print(int(Version('$VERSION').is_prerelease))") >> "$GITHUB_OUTPUT"
echo is_devrelease=$(uv run python -c "from packaging.version import Version; print(int(Version('$VERSION').is_devrelease))") >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo is_local=$(uv run python -c "from packaging.version import Version; print(int(Version('$VERSION').local is not None))") >> "$GITHUB_OUTPUT"
echo base_version=$(uv run python -c "from packaging.version import Version; print(Version('$VERSION').base_version)") >> "$GITHUB_OUTPUT"
echo fallback_base=$(uv run python -c "import tomllib; from packaging.version import Version; print(Version(tomllib.load(open('pyproject.toml', 'rb'))['tool']['hatch']['version']['fallback-version']).base_version)") >> "$GITHUB_OUTPUT"
echo major_minor_version=$(uv run python -c "from packaging.version import Version; v = Version('$VERSION'); print(f'{v.major}.{v.minor}')") >> "$GITHUB_OUTPUT"
echo latest_tag=$(curl -L \
-H "Accept: application/vnd.github+json" \
Expand All @@ -53,16 +59,23 @@ jobs:
https://api.github.com/repos/${{ github.repository }}/releases/latest \
| jq -r '.tag_name') >> "$GITHUB_OUTPUT"

- name: Check tag version
- name: "Publish guard: resolved version must match the release tag"
run: |
EXPECTED_TAG="v${{ steps.release.outputs.version }}"
if [ "${{ github.event.release.tag_name }}" != "$EXPECTED_TAG" ]; then
echo "Tag version does not match python project version"
echo "Resolved version (${{ steps.release.outputs.version }}) does not match release tag ${{ github.event.release.tag_name }}"
echo "Expected: $EXPECTED_TAG"
echo "Got: ${{ github.event.release.tag_name }}"
exit 1
fi

- name: "Publish guard: reject unreleased fallback version"
# fallback_base is read from pyproject.toml at run time; the fallback is a static sentinel (0.0.0.dev0)
if: steps.release.outputs.base_version == steps.release.outputs.fallback_base && (steps.release.outputs.is_devrelease == 1 || steps.release.outputs.is_local == 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A valid v0.0.0.devN or local tag is rejected because base_version identifies only 0.0.0, not whether Hatch used the fallback. Check exact tag reachability before rejecting the fallback version.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release.yml, line 74:

<comment>A valid `v0.0.0.devN` or local tag is rejected because `base_version` identifies only `0.0.0`, not whether Hatch used the fallback. Check exact tag reachability before rejecting the fallback version.</comment>

<file context>
@@ -53,16 +59,23 @@ jobs:
 
+      - name: "Publish guard: reject unreleased fallback version"
+        # fallback_base is read from pyproject.toml at run time; the fallback is a static sentinel (0.0.0.dev0)
+        if: steps.release.outputs.base_version == steps.release.outputs.fallback_base && (steps.release.outputs.is_devrelease == 1 || steps.release.outputs.is_local == 1)
+        run: |
+          echo "Resolved version (${{ steps.release.outputs.version }}) is an unreleased fallback (base ${{ steps.release.outputs.fallback_base }}, dev/local build): no v* tag is reachable. Refusing to publish."
</file context>

run: |
echo "Resolved version (${{ steps.release.outputs.version }}) is an unreleased fallback (base ${{ steps.release.outputs.fallback_base }}, dev/local build): no v* tag is reachable. Refusing to publish."
exit 1

- name: Check prerelease and project version
if: github.event.release.prerelease == true && steps.release.outputs.is_prerelease == 0 && steps.release.outputs.is_devrelease == 0
run: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/repository-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
repo:
- "opsmill/emma"
- "opsmill/infrahub-demo-dc"
- "opsmill/infrahub-sync"
- "INFRAHUB_CUSTOMER1_REPOSITORY"

steps:
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/schema-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
# yamllint disable rule:truthy rule:line-length
name: Schema Drift Check

# Warn-only: surfaces when the published Infrahub JSON schema has drifted from
# the formatter's committed baseline (infrahub_sdk/ctl/schema_properties.json).
# This never fails the run — it emits ::warning:: annotations and a job summary
# so a maintainer can account for the change in schema_format.py.

on:
release:
types:
- published
workflow_dispatch:

jobs:
schema-drift:
runs-on: "ubuntu-22.04"
timeout-minutes: 5
steps:
- name: "Check out repository code"
uses: "actions/checkout@v6"
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install UV
uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: uv sync --all-groups --all-extras
- name: "Check for Infrahub schema drift (warn only)"
run: uv run invoke schema-drift-check
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ dist/*

# Generated files
generated/
# Committed, generated schema models (write/read variants) must be version-controlled.
!infrahub_sdk/schema/generated/
!infrahub_sdk/schema/generated/*.py
sandbox/

# hatch-vcs version file (written at build time; must not be tracked)
infrahub_sdk/_version.py

# SpecKit internal cache
.specify/**/.cache/
.specify/feature.json
1 change: 1 addition & 0 deletions .vale/styles/Infrahub/sentence-case.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ exceptions:
- IP
- IP Fabric
- IPAM
- IPAddress
- IPHost
- IPNetwork
- JavaScript
Expand Down
1 change: 1 addition & 0 deletions .vale/styles/spelling-exceptions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ artifact_definitions
artifact_name
async
Authentik
backoff
boolean
check_definitions
class_name
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,37 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang

<!-- towncrier release notes start -->

## [1.23.0](https://github.com/opsmill/infrahub-sdk-python/tree/v1.23.0) - 2026-08-19

### Removed

- Removed `client.branch.diff_data()` from both the async and sync clients. The method relied on a `GET /api/diff/data` REST endpoint that does not exist in Infrahub, so every call returned a 404. Use `client.get_diff_tree()` to retrieve the full diff of a branch against its base branch, or `client.get_diff_summary()` for the list of changed nodes; both use the `DiffTree` GraphQL query. ([#325](https://github.com/opsmill/infrahub-sdk-python/issues/325))

### Added

- Added the Infrahub deployment ID to the `infrahubctl info` command output and a `get_server_information()` method (returning the server version and deployment ID) on the async and sync clients. ([#1017](https://github.com/opsmill/infrahub-sdk-python/issues/1017))
- Added transparent retry of HTTP 429 (rate-limited) responses on both `InfrahubClient` and `InfrahubClientSync`. Retries use jittered exponential backoff and honour a server-provided `Retry-After` header (delta-seconds or HTTP-date). The behaviour is tunable through four new `Config` fields (`rate_limit_retry_enabled`, `rate_limit_max_retries`, `rate_limit_backoff_base`, `rate_limit_backoff_max`), and a new `RateLimitError` exception is raised when retries are exhausted. ([#1124](https://github.com/opsmill/infrahub-sdk-python/issues/1124))
- Added support for tagging SDK requests with a priority via a new `X-Priority` header. A `Priority` enum (`high`, `medium`, `low`) is available from `infrahub_sdk.constants`; set `Config.priority` (env var `INFRAHUB_PRIORITY`) for a client-wide default emitted on every request, or pass `priority=` to individual operations to override it per request. When unset, no header is sent. Works identically on `InfrahubClient` and `InfrahubClientSync`. ([#1151](https://github.com/opsmill/infrahub-sdk-python/issues/1151))
- Add `infrahubctl schema format` command, an opinionated offline formatter that normalises the key ordering of schema files. Optional flags can also strip redundant default values (`--strip-defaults`), sort attributes/relationships by `order_weight` (`--sort-by-order-weight`), and backfill a missing `order_weight` (`--backfill-order-weight`).
- Added `retry()` and `cancel()` methods to the task manager. The `Task` model now exposes `available_actions` along with `can_retry` / `can_cancel` helpers.
- Added an opt-in `include_diagnostics` flag to the task manager's `all()`, `filter()`, and `get()` methods. When enabled, tasks expose an `error` field, and `webhook-send` tasks are returned as `WebhookDeliveryTask` instances carrying `http_request` / `http_response` delivery details.
- Added support for the new `IPAddress` attribute kind. Values are exposed as bare `ipaddress.IPv4Address`/`IPv6Address` objects (no prefix) and serialized to a bare-address string when writing, alongside the existing `IPHost` and `IPNetwork` kinds.
- The JSON schema generated for `.infrahub.yml` now warns when a definition has not said what it depends on, so YAML language servers flag it while the file is being edited. A Python transform or generator definition with no `watch` block is flagged, and so is a `watch` value that is not a mapping, including the bare `watch:` that parses as null and records nothing. Both warnings are advisory only: the models still accept every one of those forms. An empty `watch: {}` or `files: []` stays clean, since either one records that the author checked and nothing beyond what Infrahub detects needs watching. The generated schema also picks up the `watch` block on generator definitions, which it was previously rejecting as an unknown property.
- The request priority (`X-Priority` header) can now be carried on the client's `RequestContext` via a new `priority` field, alongside the existing client-wide `Config.priority` default and per-call `priority=` override. Resolution precedence is per-call `priority=` > `request_context.priority` > `Config.priority` > no header. The priority is emitted as a header only and is never included in the mutation body. Works identically on `InfrahubClient` and `InfrahubClientSync`.
- Import `pyarrow` lazily in the line-delimited JSON importer so that `infrahubctl` commands other than `object load` no longer require the `ctl` extra (and its heavy `pyarrow` dependency) to be installed.

### Changed

- Paginated queries generated by `all()`, `filters()`, `get()` and resource pool allocation lookups now pass `offset` and `limit` as GraphQL variables instead of inlining them in the query text. The query document stays identical across pages, allowing the Infrahub server to reuse its cached query analysis, and the query is now rendered once per call instead of once per page. `generate_query_data` also accepts variable placeholder strings (for example `"$offset"`) for its `offset` and `limit` arguments.
- Raised the default `Config.rate_limit_max_retries` from 5 to 10, so a request shed with HTTP 429 keeps retrying (honouring `Retry-After`) for longer before raising `RateLimitError`. This lets background work ride out a longer burst of server-side backpressure. Callers that prefer to give up sooner can lower the value.
- The hand-maintained schema models in `infrahub_sdk.schema` are now backed by the generated write/read contract (`infrahub_sdk.schema.generated`). Public names, import paths, and behavior methods are unchanged, but a few defaults and constraints now match the server contract:

- `AttributeKind.STRING` has been removed. It was deprecated and `kind="String"` was already rejected server-side; use `AttributeKind.TEXT` instead.
- Write and read models drop unknown fields silently (`extra="ignore"`). A submitted field that is not part of the write contract — read-level, internal, or a typo — is dropped rather than rejected, and a read model tolerates additional fields returned by a newer server.
- Write-model defaults now match the server contract: relationship `min_count`/`max_count` default to `0` (was `None`), node `branch` defaults to `"aware"`, `generate_profile` defaults to `True`, and `generate_template` defaults to `False`. This changes the round-trip output of programmatically-built schemas.

Constructing `AttributeSchema(name=..., kind=AttributeKind.TEXT, ...)`, `NodeSchema`, `GenericSchema`, `RelationshipSchema`, `SchemaRoot`, and the read-side `*API` models continues to work unchanged.

## [1.22.3](https://github.com/opsmill/infrahub-sdk-python/tree/v1.22.3) - 2026-08-19

### Fixed
Expand Down
43 changes: 43 additions & 0 deletions dev/specs/ihs-249-sdk-429-retry/alignment-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Spec/Ask Alignment Check: SDK retry with backoff on HTTP 429 responses

**Date**: 2026-07-07
**Feature**: [spec.md](./spec.md)

## 1. Source

**Source PRD**: Jira IHS-249 — "SDK retry with backoff on HTTP 429 responses"
(`https://opsmill.atlassian.net/browse/IHS-249`), fetched via the Atlassian MCP tool.
The issue body is itself a full, structured PRD (Problem Statement, Solution Overview, 9 User
Stories, 3 prioritised User Journeys with acceptance criteria, FR-001…009, Key Entities, Edge
Cases, SC-001…005, Implementation/Testing Decisions, Out of Scope, one Open Question). Related
GitHub issue: opsmill/infrahub-sdk-python#1124. No secondary URLs to fetch.

## 2. Verdict

Result: ✅ ALIGNED

`spec.md` faithfully carries every PRD requirement, acceptance criterion, and scope boundary.
The only additions are an expansion of an existing requirement and the authorized resolution of
the PRD's explicit open question — neither is drift under the check's definition.

## 3. Findings

| Severity | Category | PRD reference | Spec reference | Description |
| ---------- | ---------- | --------------- | ---------------- | ------------- |
| ✅ none | missing | FR-001…009 | FR-001…009 | All nine functional requirements present, none dropped or softened (attempt cap, jittered+clamped backoff, Retry-After both forms, malformed fallback, RateLimitError with url/attempts/last-Retry-After, all request paths, per-retry logging, async/sync parity, tune+disable). |
| ✅ none | missing | Journeys P1–P3, User Stories 1–9 | US1–US4, Edge Cases | P1/P2/P3 journeys map to US1/US2/US3; PRD user story 8 (tune/disable) surfaced as US4. All acceptance scenarios preserved. |
| ✅ none | missing | SC-001…005 | SC-001…005 | Success criteria carried over with equivalent semantics. |
| ✅ none | contradicted | Out of Scope (503, server-side INFP-636/635, `retry_on_failure`) | Out of Scope | Scope boundaries reproduced verbatim; nothing contradicted. |
| ℹ️ info | added (authorized) | Open Question (chain httpx.HTTPStatusError as `__cause__`?) | FR-005, Assumptions | The PRD's single open question was resolved affirmatively (chain the transport error as `__cause__`). The parent prep flow explicitly authorizes autonomous clarification resolution; recorded as an assumption. Not drift. |
| ℹ️ info | added (derived) | FR-009 (disable via Config) | SC-006 | Spec adds SC-006 (disabled path raises immediately). This is a measurable expansion of FR-009, not new scope. |
| ℹ️ info | added (design) | Assumption: single `_request` chokepoint | plan.md R1 / data-model | Plan (not spec) records that multipart/streaming bypass `_request`, so retry is applied at three sites. This corrects a PRD *assumption* at the implementation layer while still satisfying FR-006; spec requirements unchanged. Not spec drift. |

No requirements are missing, no acceptance criteria dropped or softened, no requirement semantics
changed, and no off-scope scope items were introduced. The Config field defaults (enabled, 5, 0.5,
60), the new `RateLimitError`, and the additive-only API surface all match the PRD exactly.

## 4. Action

**Proceed.** No remediation passes required (remediation counter: 0). `tasks.md` is safe to hand to
the implementation phase. The affirmative resolution of the open question and the SC-006 derivation
are documented above for traceability.
39 changes: 39 additions & 0 deletions dev/specs/ihs-249-sdk-429-retry/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Specification Quality Checklist: SDK retry with backoff on HTTP 429 responses

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-07-07
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- The PRD's single open question (whether the exhaustion error should chain the
underlying transport error as its cause) was resolved affirmatively and encoded
into FR-005 and the Assumptions section, so no [NEEDS CLARIFICATION] markers remain.
- Entity names in the spec are described in capability terms (e.g. "rate-limit retry
decision logic") rather than concrete class names to keep the spec implementation-agnostic;
concrete names (`RateLimitRetryHandler`, `RateLimitError`, `Config` fields) are deferred to plan.md.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The Notes claim that concrete names like RateLimitError are deferred to plan.md, but spec.md already uses RateLimitError directly (FR-005 at line 94, and lines 46/84) and names the max_retries config (lines 50/54). If the checklist's intent is a spec with no implementation/API detail, either reconcile the note or update the checklist items to match the spec's actual content.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At dev/specs/ihs-249-sdk-429-retry/checklists/requirements.md, line 39:

<comment>The Notes claim that concrete names like `RateLimitError` are deferred to plan.md, but spec.md already uses `RateLimitError` directly (FR-005 at line 94, and lines 46/84) and names the `max_retries` config (lines 50/54). If the checklist's intent is a spec with no implementation/API detail, either reconcile the note or update the checklist items to match the spec's actual content.</comment>

<file context>
@@ -0,0 +1,39 @@
+  into FR-005 and the Assumptions section, so no [NEEDS CLARIFICATION] markers remain.
+- Entity names in the spec are described in capability terms (e.g. "rate-limit retry
+  decision logic") rather than concrete class names to keep the spec implementation-agnostic;
+  concrete names (`RateLimitRetryHandler`, `RateLimitError`, `Config` fields) are deferred to plan.md.
</file context>

37 changes: 37 additions & 0 deletions dev/specs/ihs-249-sdk-429-retry/contracts/config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Contract: `Config` rate-limit fields (additive, public)

Added to `infrahub_sdk/config.py::ConfigBase`. All additive; no existing field changes.

```python
rate_limit_retry_enabled: bool = Field(
default=True,
description="Retry requests that receive HTTP 429 using backoff. Set False to disable.",
)
rate_limit_max_retries: int = Field(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The contract documents rate_limit_max_retries default as 5, but the actual field in infrahub_sdk/config.py:73 defaults to 10, and the spec/plan both say 5. Since this contract is meant to be the authoritative public surface for the 1.23.0 release, consumers will be told the wrong default. Reconcile the two: either change the field default to 5 or update the contract to 10.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At dev/specs/ihs-249-sdk-429-retry/contracts/config.md, line 10:

<comment>The contract documents `rate_limit_max_retries` default as 5, but the actual field in infrahub_sdk/config.py:73 defaults to 10, and the spec/plan both say 5. Since this contract is meant to be the authoritative public surface for the 1.23.0 release, consumers will be told the wrong default. Reconcile the two: either change the field default to 5 or update the contract to 10.</comment>

<file context>
@@ -0,0 +1,37 @@
+    default=True,
+    description="Retry requests that receive HTTP 429 using backoff. Set False to disable.",
+)
+rate_limit_max_retries: int = Field(
+    default=5,
+    ge=0,
</file context>

default=5,
ge=0,
description="Maximum number of retries after the initial attempt when receiving HTTP 429.",
)
rate_limit_backoff_base: float = Field(
default=0.5,
gt=0,
description="Base interval in seconds for exponential backoff between 429 retries.",
)
rate_limit_backoff_max: float = Field(
default=60.0,
gt=0,
description="Maximum wait in seconds for any single 429 retry (also clamps Retry-After).",
)
```

## Backward compatibility

- Purely additive; existing code constructing `Config(...)` / `InfrahubClient(...)` is unaffected.
- Environment-variable overrides follow the existing `BaseSettings` mechanism (e.g.
`INFRAHUB_RATE_LIMIT_MAX_RETRIES`), consistent with current fields.

## Guarantees

- `rate_limit_retry_enabled=False` ⇒ a 429 is returned/raised exactly as before this feature
(no wait, no extra attempt). (FR-009, SC-006)
- Defaults produce transparent retry for typical background workloads. (FR-001)
Loading
Loading