Skip to content

feat: add X-Priority request header to the SDK#1167

Open
dgarros wants to merge 22 commits into
developfrom
dga/feat-x-priority-aa2nd
Open

feat: add X-Priority request header to the SDK#1167
dgarros wants to merge 22 commits into
developfrom
dga/feat-x-priority-aa2nd

Conversation

@dgarros

@dgarros dgarros commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Why

Infrahub's background systems (generators, artifacts, diffs, syncs, computed attributes) call back into the same API servers through this SDK, competing on equal footing with human/frontend traffic for a shared worker pool and database connections. Today the API layer cannot tell interactive traffic from background traffic, so under heavy background load it cannot preferentially protect the frontend.

Goal: let the originator of each request — the SDK caller — declare how important the request is, in a form the server can act on. This is the SDK's contribution to the server-side prioritization effort in INFP-636.

Non-goals: no server-side admission control, no 429/Retry-After handling (that is GitHub #1124), no traffic-classification guidance.

Closes #1151

What changed

Behavioral changes (what callers can now do):

  • Client-wide default priority. Set Config.priority (or INFRAHUB_PRIORITY, case-insensitive) and every request from that client carries X-Priority: <value> across all transports — GraphQL, multipart upload, and raw blob _get/_post — with no call-site changes. Ideal for a client dedicated to background work (priority=Priority.LOW).
  • Per-request override. Pass priority=Priority.HIGH to get, all, execute_graphql, the diff methods, and node save/create/update/delete to override the default for exactly one request; the next un-annotated call reverts to the client default.
  • Typed contract. A new Priority enum (high/normal/low) is exported from infrahub_sdk, so intent is expressed as a value rather than a raw header string; an invalid configured value is rejected at config-load time.
  • Zero behaviour change when unset. With no priority configured and none passed per request, no X-Priority header is emitted — outgoing requests are unchanged from before this PR.

Implementation notes:

  • The default is injected once into the client's base header set, so it rides every transport automatically (mirrors how X-INFRAHUB-KEY already works). The per-request override is applied only in the two GraphQL funnels; higher-level methods forward the kwarg and never re-resolve. Resolution is per_request if per_request is not None else client_default.
  • Both InfrahubClient (async) and InfrahubClientSync (sync) behave identically.

What stayed the same:

  • No new dependencies. No schema/API-contract changes beyond the additive Priority enum, Config.priority field, and optional priority= kwarg. Generated protocols.py untouched.
  • An included fix keeps the automatic relogin token-refresh working for password-auth clients after the header-merge change (a per-request header snapshot must not shadow a freshly-refreshed Authorization).

Related context

Built with spec-kit; the spec, plan, critique, task breakdown, and implementation report live under dev/specs/ihs-259-sdk-x-priority-header/. Server contract per INFP-636 (header is exactly X-Priority; absent/unknown treated as normal server-side, which is why omitting the header when unset is a safe, incremental rollout). Complementary: GitHub #1124 (429 retry/backoff).

How to review

Core change is small and funnels through a few points:

  1. infrahub_sdk/constants.py — the Priority enum + case-insensitive _missing_.
  2. infrahub_sdk/config.py — the priority field.
  3. infrahub_sdk/client.py — base-header injection in BaseClient.__init__, the priority= handling in execute_graphql/_execute_graphql_with_file, and _merge_request_headers (per-request headers win, but live auth is re-asserted so relogin retries use the fresh token).
  4. infrahub_sdk/node/node.pypriority= forwarding on save/create/update/delete.

The docs/ .mdx changes are generated (uv run invoke docs-generate).

How to test

uv run pytest tests/unit/sdk/test_priority.py tests/unit/sdk/test_config.py tests/unit/sdk/test_relogin_headers.py -q

Expected: green across both standard (async) and sync clients. The suite asserts the header on the wire per transport, no header when unconfigured, per-request override + revert, explicit NORMAL beating a LOW default, config validation, the full resolution truth table, and the relogin-token-refresh regression. Full SDK suite (uv run pytest tests/unit/sdk/) is green (1145 passed).

Impact & rollout

  • Backward compatibility: fully additive. Callers who set nothing see byte-identical outgoing requests (no X-Priority). Safe to deploy independently of the server work.
  • Performance: negligible — one dict insertion per request, no new round-trips.
  • Config/env changes: new optional Config.priority field / INFRAHUB_PRIORITY env var (default: unset).
  • Deployment notes: safe to deploy; the header is inert until the server (INFP-636) acts on it.

Checklist

  • Tests added/updated
  • Changelog entry added (changelog/1151.added.md)
  • External docs updated (regenerated SDK reference)
  • Internal .md docs updated (spec-kit artifacts under dev/specs/)

Summary by cubic

Adds an X-Priority header to tag SDK requests as high, normal, or low for server-side prioritization. Header is omitted when unset; async and sync clients behave the same.

  • New Features

    • Priority enum exported from infrahub_sdk.
    • Client-wide default via Config.priority or INFRAHUB_PRIORITY; applied to GraphQL, multipart uploads, blob _get/_post, and the count query used by all(parallel=True).
    • Per-request override with priority on get, all, count, execute_graphql, create_diff, get_diff_summary, get_diff_tree, and node save/create/update/delete; resource‑pool peer fetch after node mutations inherits the operation’s priority.
    • Resolution: per-request beats default; if neither set, no X-Priority. No new dependencies.
  • Bug Fixes

    • Fixed relogin for password-auth clients by preserving the refreshed Authorization when merging per-request headers.

Written for commit 8b09904. Summary will update on new commits.

Review in cubic

dgarros and others added 17 commits July 12, 2026 08:10
Phase 1 (Specify) of speckit prep: spec.md + requirements checklist for
the SDK X-Priority feature — client-wide default + per-request override,
zero behaviour change when unconfigured, async/sync parity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 (Plan): plan.md, research.md, data-model.md, quickstart.md, and
API + wire contracts. Approach grounded in the existing X-Infrahub-Tracker
prior art — default injected into base self.headers, per-request override
applied in execute_graphql/_execute_graphql_with_file only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-259)

Phase 3 (Critique): dual-lens (product + engineering) review — verdict
PROCEED WITH UPDATES, no blockers. Applied low-risk recommendations:
- SC-002 reworded (X-Priority absent vs literal byte-for-byte)
- new SC-006 (batch/blob inherit the client default)
- scoped the manual-header edge case to low-level _get/_post
- plan/research notes: pagination forwards priority per page; multipart
  sets X-Priority after the content-type pop

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…259)

Phase 4 (Tasks): 35 tasks organized by user story (US1 MVP → US5 parity)
plus setup, foundational, and polish. Tests included per PRD testing
decisions; critique follow-ups (pagination, multipart ordering,
batch/blob inheritance) captured as explicit test tasks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 5: verdict ALIGNED against the IHS-259 PRD. All FRs, SCs, user
stories, edge cases, and out-of-scope items carried over; only expansions
(FR-009, SC-006) and one testability clarification (SC-002). 0 remediation
passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a case-insensitive Priority(str, Enum) in constants.py, re-export it
from the SDK public namespace, and add a Config.priority field
(auto-binding to INFRAHUB_PRIORITY, default None). Foundational prereqs
T002-T004 for the X-Priority request header feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Inject the client-wide default priority once in BaseClient.__init__ so it
rides GraphQL, multipart, and raw blob transports via the shared
self.headers merge. Add unit tests covering GraphQL query/mutation,
object-store blob download/upload, multipart upload, batched requests,
and the always-emitted normal default, for both async and sync clients.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lock backwards compatibility (US3): an unconfigured client emits no
X-Priority header across GraphQL, multipart, and blob transports, and
its baseline SDK headers (content-type, X-Infrahub-Tracker) are
unchanged. Both async and sync clients.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…S-259]

Add a `priority: Priority | None = None` keyword to the GraphQL execute
funnels (execute_graphql, _execute_graphql_with_file) and thread it through
the high-level client methods (get, all, filters, create_diff,
get_diff_summary, get_diff_tree) and node methods (save, create, update,
delete) on both the async and sync clients. Resolution is
`per_request if per_request is not None else client_default`.

Per-request headers now take precedence over self.headers in the transport
helpers (_post/_get/_get_streaming/_post_multipart) so an explicit override
is not clobbered by the client-wide default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… unknown [IHS-259]

Lock the Priority enum + pydantic validation for Config.priority (US4):
- unknown value ("lowe") raises ValidationError at config load (no request)
- "LOW"/"Low"/"low"/Priority.LOW all resolve to Priority.LOW (and HIGH/NORMAL)
- INFRAHUB_PRIORITY env-var path resolves case-insensitively
- default Config() leaves priority None (header omitted)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…le [IHS-259]

Add a data-driven parity test encoding the resolution truth table from
data-model.md (8 default x override rows), run against both the async and
sync clients (16 param cases) asserting identical emitted X-Priority
headers. Audited T006-T012 and T021-T024: all already parametrized over
["standard","sync"], no change needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… [IHS-259]

Phase 8 polish for the X-Priority feature:
- Add priority kwarg mention to get_diff_tree docstrings (async + sync)
- Regenerate SDK docs (new Config.priority field in config.mdx; priority
  kwarg across client/node reference), also picking up pre-existing
  generator whitespace drift so docs-validate passes
- Add changelog fragment 1151.added.md describing the Priority enum,
  Config.priority, and per-request priority= surface

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rge [IHS-259]

The X-Priority feature flipped the transport-helper header merge so a
per-request snapshot wins over the client base headers. For password-auth
clients this regressed the automatic relogin retry: handle_relogin refreshes
self.headers["Authorization"] to a new token, but the retry re-sent the SAME
stale snapshot, which then overwrote the fresh token -> the retry used the
expired token and auth failed.

Introduce BaseClient._merge_request_headers, used at every transport-helper
merge site (async/sync _post, _get, _get_streaming, _post_multipart). It lets
per-request headers win (preserving the X-Priority override) but then
re-asserts Authorization / X-INFRAHUB-KEY from self.headers so a mid-flight
refreshed token always beats a stale snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… override [IHS-259]

Add per-request X-Priority override tests for node.delete(), the save() update
path on an existing node, and get_diff_summary()/get_diff_tree() (create_diff was
already covered). All parametrized over async and sync clients.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final report for the implement + review tail: 35/35 tasks done, 1 HIGH
review finding (relogin auth-header regression) fixed inline, 3 test-gap
findings closed. SDK suite 1145 passed; 8 pre-existing unrelated failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…HS-259]

Re-run docs-generate on top of develop so the committed generated docs
(config.mdx priority field, client/node method priority kwarg) match the
current generator and markdown lint rules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dgarros dgarros added the type/feature New feature or request label Jul 12, 2026
@github-actions github-actions Bot added the type/documentation Improvements or additions to documentation label Jul 12, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 12, 2026

Copy link
Copy Markdown

Deploying infrahub-sdk-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8b09904
Status: ✅  Deploy successful!
Preview URL: https://c73a8ead.infrahub-sdk-python.pages.dev
Branch Preview URL: https://dga-feat-x-priority-aa2nd.infrahub-sdk-python.pages.dev

View logs

@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.87500% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
infrahub_sdk/constants.py 90.90% 0 Missing and 1 partial ⚠️
infrahub_sdk/node/node.py 92.30% 1 Missing ⚠️
@@             Coverage Diff             @@
##           develop    #1167      +/-   ##
===========================================
- Coverage    82.92%   82.78%   -0.15%     
===========================================
  Files          139      139              
  Lines        12688    12210     -478     
  Branches      1906     1834      -72     
===========================================
- Hits         10522    10108     -414     
+ Misses        1598     1538      -60     
+ Partials       568      564       -4     
Flag Coverage Δ
integration-tests 40.66% <43.75%> (-1.33%) ⬇️
python-3.10 56.37% <67.18%> (-0.12%) ⬇️
python-3.11 56.38% <67.18%> (-0.10%) ⬇️
python-3.12 56.37% <67.18%> (-0.12%) ⬇️
python-3.13 56.38% <67.18%> (-0.09%) ⬇️
python-3.14 56.37% <67.18%> (-0.10%) ⬇️
python-filler-3.12 22.53% <28.12%> (-0.17%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
infrahub_sdk/__init__.py 80.00% <100.00%> (+2.22%) ⬆️
infrahub_sdk/client.py 79.43% <100.00%> (+3.04%) ⬆️
infrahub_sdk/config.py 91.46% <100.00%> (+0.05%) ⬆️
infrahub_sdk/node/related_node.py 91.09% <100.00%> (ø)
infrahub_sdk/constants.py 93.33% <90.90%> (-6.67%) ⬇️
infrahub_sdk/node/node.py 87.57% <92.30%> (ø)

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

dgarros and others added 2 commits July 12, 2026 08:17
rumdl (MD022/MD058) flagged missing blank lines around headings and
tables in the auto-generated critique. Apply rumdl auto-fix so the
markdown-lint CI job passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The file is gitignored on develop but was committed on this branch before
that rule; untrack it while keeping the local working copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

Review completed against the latest diff

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread dev/specs/ihs-259-sdk-x-priority-header/contracts/priority-api.md Outdated
Comment thread infrahub_sdk/client.py
Comment thread dev/specs/ihs-259-sdk-x-priority-header/checklists/requirements.md
Comment thread docs/docs/python-sdk/reference/config.mdx
Comment thread tests/unit/sdk/test_config.py Outdated
Comment thread infrahub_sdk/node/node.py
Comment thread dev/specs/ihs-259-sdk-x-priority-header/contracts/priority-api.md
Comment thread dev/specs/ihs-259-sdk-x-priority-header/data-model.md
Comment thread changelog/1151.added.md Outdated
@dgarros dgarros marked this pull request as ready for review July 12, 2026 08:34
@dgarros dgarros requested a review from a team as a code owner July 12, 2026 08:34
dgarros and others added 3 commits July 12, 2026 09:48
… [IHS-259]

Address review feedback: a per-request priority now rides consistently
across a whole operation.

- count() (both clients) accepts priority and forwards it; all(parallel=True)
  passes priority to its preliminary count query (previously sent at the
  client default while the pages used the override).
- Resource-pool peer fetch after node create/update/save now inherits the
  operation's priority via _process_mutation_result -> RelatedNode.fetch.

Tests cover count (direct + parallel all) and RelatedNode.fetch forwarding,
both async and sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… [IHS-259]

- Changelog: concise entry, no longer enumerates every method.
- contracts/priority-api.md: remove the incorrect client.create(priority=)
  signature (it issues no request); document count/filters and the
  intentional exclusion; note peer-fetch inherits priority.
- data-model.md: add node create() + resource-pool peer fetch to the
  coverage table; note client.create is n/a.
- Regenerate SDK reference docs for the new count/fetch priority params.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… [IHS-259]

Pass the deliberately-dynamic priority input through a dict[str, Any] so the
runtime-coercion tests type-check cleanly, instead of suppressing the checker
with an ignore comment. Behaviour and assertions unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread infrahub_sdk/__init__.py
"Config",
"InfrahubClient",
"InfrahubClientSync",
"Priority",

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.

I'd say, don't add this here. it makes it so that importing Priority requires importing Config and the clients

Comment thread infrahub_sdk/client.py
Comment on lines +714 to +715
Args:
priority: Override the client-wide request priority for this query. When None, the client default is used.

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.

seems weird to document just 1 arg

Comment thread infrahub_sdk/client.py
return InfrahubClient(config=self.config.clone(branch=branch))

async def execute_graphql(
async def execute_graphql( # noqa: PLR0912

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.

maybe this can be refactored to build the payload or headers or do the retry loop in a helper method to remove this typing ignore.

Comment thread infrahub_sdk/client.py
Comment on lines +3425 to +3427
Args:
priority: Per-request priority emitted as the X-Priority header, overriding the client
default for this request only. When None, the client default (if any) is used.

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.

same here

Comment thread infrahub_sdk/constants.py
Comment on lines +15 to +17
String-valued closed enum accepting values case-insensitively (e.g. "LOW",
"Low" and "low" all resolve to :attr:`Priority.LOW`). Unknown values raise
``ValueError``, which surfaces as a ``pydantic.ValidationError`` at config load.

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.

"Case-insensitive" covers it. it's obviously a string, we know what case-insensitive means, raising a ValueError is standard enum behavior, mentioning the "config" load is not the responsibility of this class


@pytest.mark.parametrize("client_type", client_types)
@pytest.mark.parametrize("case", [pytest.param(tc, id=tc.name) for tc in RESOLUTION_TRUTH_TABLE])
async def test_resolution_truth_table_parity(case: ResolutionCase, client_type: str, httpx_mock: HTTPXMock) -> None:

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.

looks like this makes some of the execute_graphql tests above unnecessary, but I could be missing something

if client_type == "standard":
result = await client.count(kind="CoreRepository", priority=Priority.HIGH)
else:
result = client.count(kind="CoreRepository", priority=Priority.HIGH)

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.

does this need to assert the priority on the requests or is match_headers in the httpx_mock enough?

if isinstance(client, InfrahubClient):
await client.execute_graphql(query=query, branch_name="main")
else:
client.execute_graphql(query=query, branch_name="main")

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.

should this test include the priority override that seems to have semi-caused the issue? or the Authorization / X-INFRAHUB-KEY header?

Comment thread CLAUDE.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.

leftover

Comment thread infrahub_sdk/client.py
for auth_key in ("Authorization", "X-INFRAHUB-KEY"):
if auth_key in self.headers:
merged[auth_key] = self.headers[auth_key]
return merged

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.

I'm not entirely sure I understand how this issue might arise. the @handle_relogin calls Client.login, which sets self.headers["Authorization"]. so it seems like this would only be an issue if someone was including their auth information in the headers dict for every individual request. in which case, I would think that we should respect that b/c the user would be affirmatively forcing themself to handle relogin on their own. preventing them from setting a per-request override for these two keys feels wrong

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/documentation Improvements or additions to documentation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants