diff --git a/changelog/1151.added.md b/changelog/1151.added.md
index d4cba33c6..3180fe0d1 100644
--- a/changelog/1151.added.md
+++ b/changelog/1151.added.md
@@ -1 +1 @@
-Added support for tagging SDK requests with a priority via a new `X-Priority` header. A `Priority` enum (`high`, `normal`, `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`.
+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`.
diff --git a/dev/specs/ihs-259-sdk-x-priority-header/contracts/priority-api.md b/dev/specs/ihs-259-sdk-x-priority-header/contracts/priority-api.md
index 9d4b8206b..5181aa9d9 100644
--- a/dev/specs/ihs-259-sdk-x-priority-header/contracts/priority-api.md
+++ b/dev/specs/ihs-259-sdk-x-priority-header/contracts/priority-api.md
@@ -9,7 +9,7 @@ from infrahub_sdk.constants import Priority
class Priority(str, enum.Enum):
HIGH = "high"
- NORMAL = "normal"
+ MEDIUM = "medium"
LOW = "low"
```
@@ -25,7 +25,7 @@ class ConfigBase(BaseSettings):
priority: Priority | None = Field(
default=None,
description="Default request priority emitted as the X-Priority header on every request. "
- "One of high|normal|low (case-insensitive). When unset, no header is sent.",
+ "One of high|medium|low (case-insensitive). When unset, no header is sent.",
)
```
diff --git a/dev/specs/ihs-259-sdk-x-priority-header/contracts/x-priority-header.md b/dev/specs/ihs-259-sdk-x-priority-header/contracts/x-priority-header.md
index f9acfc0c1..e6eb99e64 100644
--- a/dev/specs/ihs-259-sdk-x-priority-header/contracts/x-priority-header.md
+++ b/dev/specs/ihs-259-sdk-x-priority-header/contracts/x-priority-header.md
@@ -7,7 +7,7 @@
| Property | Value |
|----------|-------|
| Name | `X-Priority` (exact, case-insensitive on the server per HTTP header rules) |
-| Values | `high`, `normal`, `low` (lowercase emitted by the SDK) |
+| Values | `high`, `medium`, `low` (lowercase emitted by the SDK) |
| Cardinality | 0 or 1 per request |
## Emission rules (SDK side)
@@ -20,8 +20,8 @@
## Server semantics (assumed, per INFP-636 — not implemented here)
- The server treats the value case-insensitively.
-- An **absent** header and an **unknown** value are both treated as `normal`.
-- Consequently, "omit the header" and "send `normal`" are server-equivalent, which is what makes omitting-when-unconfigured a safe, non-breaking rollout.
+- An **absent** header and an **unknown** value are both treated as `medium`.
+- Consequently, "omit the header" and "send `medium`" are server-equivalent, which is what makes omitting-when-unconfigured a safe, non-breaking rollout.
## Non-goals (this contract)
diff --git a/dev/specs/ihs-259-sdk-x-priority-header/critiques/critique-20260710-164718.md b/dev/specs/ihs-259-sdk-x-priority-header/critiques/critique-20260710-164718.md
index f33b9afbb..66d084e10 100644
--- a/dev/specs/ihs-259-sdk-x-priority-header/critiques/critique-20260710-164718.md
+++ b/dev/specs/ihs-259-sdk-x-priority-header/critiques/critique-20260710-164718.md
@@ -31,7 +31,7 @@ This is a small, well-scoped, low-risk feature backed by a detailed PRD (IHS-259
| ID | Severity | Finding | Suggestion |
|----|----------|---------|------------|
-| P3 | 🤔 | A `Literal["high","normal","low"]` would be simpler than an enum. | Rejected by design — FR-001/PRD mandate a `Priority` enum for typo-safety and intent expression. No action. |
+| P3 | 🤔 | A `Literal["high","medium","low"]` would be simpler than an enum. | Rejected by design — FR-001/PRD mandate a `Priority` enum for typo-safety and intent expression. No action. |
### Edge Cases & UX
diff --git a/dev/specs/ihs-259-sdk-x-priority-header/data-model.md b/dev/specs/ihs-259-sdk-x-priority-header/data-model.md
index 47784bff7..d2f7cfb3e 100644
--- a/dev/specs/ihs-259-sdk-x-priority-header/data-model.md
+++ b/dev/specs/ihs-259-sdk-x-priority-header/data-model.md
@@ -11,7 +11,7 @@ A closed enumeration representing request priority. Owned by the SDK; no lifecyc
| Member | Wire value | Meaning |
|----------|-----------|----------------------------------------------------------------|
| `HIGH` | `high` | Prefer this request; server should protect it under load. |
-| `NORMAL` | `normal` | Default server treatment; equivalent to absent header. |
+| `MEDIUM` | `medium` | Default server treatment; equivalent to absent header. |
| `LOW` | `low` | Sheddable first; intended for background/bulk workloads. |
- **Base type**: `str, enum.Enum` — each member *is* its lowercase wire token, so it drops directly into a header dict.
@@ -32,7 +32,7 @@ Extends `ConfigBase` (`infrahub_sdk/config.py`).
| Type | `Priority \| None` |
| Default | `None` (no client-wide default → header omitted) |
| Env var | `INFRAHUB_PRIORITY` (via `env_prefix="INFRAHUB_"`) |
-| Accepts | a `Priority` value, or a case-insensitive string (`"high"/"normal"/"low"`, any case) |
+| Accepts | a `Priority` value, or a case-insensitive string (`"high"/"medium"/"low"`, any case) |
| Validation | pydantic + `Priority` enum; unknown value → `ValidationError` at load time |
| Carried by `clone()` | Yes (automatic — `clone()` iterates `Config.model_fields`) |
@@ -54,9 +54,9 @@ The client's base header dict, built once in `BaseClient.__init__` (`infrahub_sd
| Attribute | Value |
|-----------|-------|
| Header name | `X-Priority` (exact) |
-| Value | one of `high` / `normal` / `low` (lowercase) |
+| Value | one of `high` / `medium` / `low` (lowercase) |
| Presence | present only when the resolved priority is non-`None` |
-| Server semantics (per INFP-636) | case-insensitive; absent or unknown treated as `normal` |
+| Server semantics (per INFP-636) | case-insensitive; absent or unknown treated as `medium` |
See [contracts/x-priority-header.md](./contracts/x-priority-header.md).
@@ -78,14 +78,14 @@ Realised in code as: the client default is already in the copied `self.headers`;
|----------------|-----------------|-----------------------|
| `None` | `None` | *(none)* |
| `None` | `HIGH` | `X-Priority: high` |
-| `None` | `NORMAL` | `X-Priority: normal` |
+| `None` | `MEDIUM` | `X-Priority: medium` |
| `LOW` | `None` | `X-Priority: low` |
| `LOW` | `HIGH` | `X-Priority: high` |
-| `LOW` | `NORMAL` | `X-Priority: normal` (explicit step-up wins) |
-| `NORMAL` | `None` | `X-Priority: normal` |
+| `LOW` | `MEDIUM` | `X-Priority: medium` (explicit step-up wins) |
+| `MEDIUM` | `None` | `X-Priority: medium` |
| `HIGH` | `LOW` | `X-Priority: low` |
-- There is no per-request way to force "send no header" once a default is set; passing `NORMAL` explicitly is the accepted equivalent (spec Edge Cases).
+- There is no per-request way to force "send no header" once a default is set; passing `MEDIUM` explicitly is the accepted equivalent (spec Edge Cases).
- A per-request value never mutates client state — the next un-annotated call reverts to the client default (SC-003).
## Coverage of the per-request override
diff --git a/dev/specs/ihs-259-sdk-x-priority-header/opsmill-implement-report.md b/dev/specs/ihs-259-sdk-x-priority-header/opsmill-implement-report.md
index 6e82882d1..2c2fb634e 100644
--- a/dev/specs/ihs-259-sdk-x-priority-header/opsmill-implement-report.md
+++ b/dev/specs/ihs-259-sdk-x-priority-header/opsmill-implement-report.md
@@ -49,7 +49,7 @@ All tests added/modified by this run, observed passing locally (unit; project ha
| `test_priority.py::test_priority_header_on_blob_upload` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
| `test_priority.py::test_priority_header_on_multipart_upload` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
| `test_priority.py::test_priority_header_on_batched_requests` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
-| `test_priority.py::test_priority_normal_is_always_emitted` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
+| `test_priority.py::test_priority_medium_is_always_emitted` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
| `test_priority.py::test_no_priority_header_on_graphql_when_unconfigured` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
| `test_priority.py::test_no_priority_header_on_blob_download_when_unconfigured` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
| `test_priority.py::test_no_priority_header_on_blob_upload_when_unconfigured` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
@@ -57,7 +57,7 @@ All tests added/modified by this run, observed passing locally (unit; project ha
| `test_priority.py::test_unconfigured_headers_unchanged_versus_baseline` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
| `test_priority.py::test_override_on_no_default_client_then_no_leak` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
| `test_priority.py::test_override_beats_default_then_reverts` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
-| `test_priority.py::test_override_normal_beats_low_default` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
+| `test_priority.py::test_override_medium_beats_low_default` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
| `test_priority.py::test_override_on_get` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
| `test_priority.py::test_override_on_all_carries_on_every_page` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
| `test_priority.py::test_override_on_save_create_path` (×2) | unit | `uv run pytest tests/unit/sdk/test_priority.py -q` | 2026-07-11T15:12:59Z | n/a | `Pytest: 64 passed` |
diff --git a/dev/specs/ihs-259-sdk-x-priority-header/plan.md b/dev/specs/ihs-259-sdk-x-priority-header/plan.md
index 23fae45f2..c24b35bcb 100644
--- a/dev/specs/ihs-259-sdk-x-priority-header/plan.md
+++ b/dev/specs/ihs-259-sdk-x-priority-header/plan.md
@@ -6,14 +6,14 @@
## Summary
-Add a first-class request-priority concept to the SDK, emitted as an `X-Priority: high|normal|low` HTTP header. Two configuration surfaces: a client-wide default via `Config.priority` (rides every transport by being injected into the client's base `self.headers`) and a per-request `priority=` keyword argument on the covered public methods (resolved as `per_request if per_request is not None else client_default`). When nothing is configured, no header is emitted — byte-for-byte identical to today. Both `InfrahubClient` (async) and `InfrahubClientSync` (sync) behave identically. No server-side logic, no 429 handling.
+Add a first-class request-priority concept to the SDK, emitted as an `X-Priority: high|medium|low` HTTP header. Two configuration surfaces: a client-wide default via `Config.priority` (rides every transport by being injected into the client's base `self.headers`) and a per-request `priority=` keyword argument on the covered public methods (resolved as `per_request if per_request is not None else client_default`). When nothing is configured, no header is emitted — byte-for-byte identical to today. Both `InfrahubClient` (async) and `InfrahubClientSync` (sync) behave identically. No server-side logic, no 429 handling.
**Technical approach** (grounded in the existing `X-Infrahub-Tracker` prior art):
-1. New `Priority(str, enum.Enum)` in `infrahub_sdk/constants.py` with members `HIGH="high"`, `NORMAL="normal"`, `LOW="low"` and a case-insensitive `_missing_` classmethod.
+1. New `Priority(str, enum.Enum)` in `infrahub_sdk/constants.py` with members `HIGH="high"`, `MEDIUM="medium"`, `LOW="low"` and a case-insensitive `_missing_` classmethod.
2. New `Config.priority: Priority | None = None` field (auto-binds to `INFRAHUB_PRIORITY` via the existing `env_prefix`). Pydantic + the enum give validation and case-insensitive string coercion for free; unknown values raise at config load.
3. Inject the configured default once into `BaseClient.__init__`'s base `self.headers` (right next to the `X-INFRAHUB-KEY` line). Because every transport already merges `self.headers`, the default automatically rides GraphQL, multipart upload, and raw blob `_get`/`_post`.
-4. Add `priority: Priority | None = None` to `execute_graphql` and `_execute_graphql_with_file` (async + sync). These are the single points where the per-request header is applied: `if priority is not None: headers["X-Priority"] = priority.value` on the already-copied header dict — which realises the resolution rule exactly (a `None` per-request keeps whatever the base default was; an explicit value, including `NORMAL`, overrides it).
+4. Add `priority: Priority | None = None` to `execute_graphql` and `_execute_graphql_with_file` (async + sync). These are the single points where the per-request header is applied: `if priority is not None: headers["X-Priority"] = priority.value` on the already-copied header dict — which realises the resolution rule exactly (a `None` per-request keeps whatever the base default was; an explicit value, including `MEDIUM`, overrides it).
5. Thread the `priority` kwarg through the higher-level callers so they forward it to the two execute methods: client `get`, `all` (via `filters`), `create`, `create_diff`/`get_diff_summary`/`get_diff_tree`; node `save`/`create`/`update`/`delete`. Raw blob `_get`/`_post` and batch mode inherit the client default only (no per-call override in v1).
**Two load-bearing details surfaced by the critique** (see [critiques/](./critiques/)):
@@ -107,7 +107,7 @@ See [research.md](./research.md) for the full decision log. Highlights:
- **Config field is named `priority`, not `x_priority`** — the PRD specifies `Config.priority`; the `X-` prefix belongs to the wire header, not the config surface. Auto-binds to `INFRAHUB_PRIORITY`.
- **Default injected into base `self.headers`, not at each call site** — this is the mechanism that guarantees FR-003 (every transport) without touching blob/batch code paths, and mirrors how `X-INFRAHUB-KEY` already rides every request.
- **Per-request application lives in `execute_graphql` / `_execute_graphql_with_file` only** — every high-level method funnels through these two, so the resolution rule is implemented once per client (twice total) instead of at ~10 call sites. Higher-level methods only *forward* the kwarg.
-- **Resolution is realised by override-if-present on the copied header dict** — `copy.copy(self.headers)` already carries the default; `if priority is not None: headers["X-Priority"] = priority.value` yields exactly `per_request if per_request is not None else client_default`, including the explicit-`NORMAL`-beats-`low`-default edge case.
+- **Resolution is realised by override-if-present on the copied header dict** — `copy.copy(self.headers)` already carries the default; `if priority is not None: headers["X-Priority"] = priority.value` yields exactly `per_request if per_request is not None else client_default`, including the explicit-`MEDIUM`-beats-`low`-default edge case.
- **Case-insensitivity via `Priority._missing_`** — handles `LOW`/`Low`/`low` from env/file config and raises for unknown values, satisfying FR-002 and FR-007 with no bespoke validator.
## Complexity Tracking
diff --git a/dev/specs/ihs-259-sdk-x-priority-header/quickstart.md b/dev/specs/ihs-259-sdk-x-priority-header/quickstart.md
index 241b370dc..f2f76b34f 100644
--- a/dev/specs/ihs-259-sdk-x-priority-header/quickstart.md
+++ b/dev/specs/ihs-259-sdk-x-priority-header/quickstart.md
@@ -41,8 +41,8 @@ client = InfrahubClient(config=Config(address="http://localhost:8000", priority=
# This one user-triggered call steps up to high; the rest stay low.
node = await client.get(kind="BuiltinTag", name__value="blue", priority=Priority.HIGH)
-# Explicit NORMAL beats a LOW default for this call only:
-await client.execute_graphql(query=MY_QUERY, priority=Priority.NORMAL) # -> X-Priority: normal
+# Explicit MEDIUM beats a LOW default for this call only:
+await client.execute_graphql(query=MY_QUERY, priority=Priority.MEDIUM) # -> X-Priority: medium
```
### Zero behaviour change when unconfigured (P1)
@@ -72,7 +72,7 @@ uv run pytest tests/unit/sdk/test_priority.py tests/unit/sdk/test_config.py \
| Default rides GraphQL, multipart, blob | `httpx_mock.add_response(match_headers={"X-Priority": "low"})` for each transport; request only matches if header present | SC-001 |
| Unconfigured client emits no header | capture request via `httpx_mock.get_requests()`, assert `"x-priority" not in request.headers` | SC-002 |
| Per-request override, then revert | override call matches `{"X-Priority": "high"}`; next un-annotated call matches the default (or no header) | SC-003 |
-| Explicit `NORMAL` beats `LOW` default | override call matches `{"X-Priority": "normal"}` | SC-003 (edge) |
+| Explicit `MEDIUM` beats `LOW` default | override call matches `{"X-Priority": "medium"}` | SC-003 (edge) |
| Invalid value rejected | `pytest.raises(pydantic.ValidationError, match=...)` on `Config(priority="lowe")` | SC-004 |
| Case-insensitive config accepted | `Config(priority="LOW").priority is Priority.LOW` | FR-002 |
| Async/sync parity | parametrize every wire test over `["standard", "sync"]` via the `BothClients` fixture | SC-005 |
diff --git a/dev/specs/ihs-259-sdk-x-priority-header/research.md b/dev/specs/ihs-259-sdk-x-priority-header/research.md
index e4b1ea6ee..37e062717 100644
--- a/dev/specs/ihs-259-sdk-x-priority-header/research.md
+++ b/dev/specs/ihs-259-sdk-x-priority-header/research.md
@@ -6,17 +6,17 @@ This feature has no external unknowns — the wire contract is fixed by INFP-636
## Decision 1 — Where the `Priority` enum lives and its shape
-- **Decision**: `class Priority(str, enum.Enum)` in `infrahub_sdk/constants.py`, members `HIGH = "high"`, `NORMAL = "normal"`, `LOW = "low"`, plus a case-insensitive `_missing_` classmethod.
+- **Decision**: `class Priority(str, enum.Enum)` in `infrahub_sdk/constants.py`, members `HIGH = "high"`, `MEDIUM = "medium"`, `LOW = "low"`, plus a case-insensitive `_missing_` classmethod.
- **Rationale**: `constants.py` already hosts `InfrahubClientMode(str, enum.Enum)` (`constants.py:4`) and is already imported by both `config.py` (`config.py:11`) and `client.py`. A `str`-valued enum means `Priority.LOW.value == "high"`-style access gives the exact wire token, and the member *is* a `str` so it slots straight into a headers dict. `_missing_` lets `Priority("LOW")` resolve case-insensitively, which pydantic uses when coercing env/file strings.
- **Alternatives considered**:
- `infrahub_sdk/enums.py` (`OrderDirection`, `enums.py:4`) — viable, but `constants.py` is the home for *client/config-consumed* enums (`InfrahubClientMode`), which is exactly this case.
- - A `Literal["high","normal","low"]` instead of an enum — rejected: FR-001 explicitly requires a `Priority` enum so callers write `Priority.LOW` and cannot typo the contract.
+ - A `Literal["high","medium","low"]` instead of an enum — rejected: FR-001 explicitly requires a `Priority` enum so callers write `Priority.LOW` and cannot typo the contract.
```python
# infrahub_sdk/constants.py
class Priority(str, enum.Enum):
HIGH = "high"
- NORMAL = "normal"
+ MEDIUM = "medium"
LOW = "low"
@classmethod
@@ -58,7 +58,7 @@ if priority is not None:
headers["X-Priority"] = priority.value
```
-- **Rationale**: these two methods are the single funnel for all GraphQL traffic. `copy.copy(self.headers)` already carries the client default, so `if priority is not None: headers["X-Priority"] = priority.value` computes exactly `resolved = per_request if per_request is not None else client_default` (FR-006): `None` keeps the default (or absence); an explicit value overrides it, including an explicit `NORMAL` stepping *up* from a `low` default (spec edge case, SC-003). Implementing it here means the rule exists twice (async + sync), not at every public method.
+- **Rationale**: these two methods are the single funnel for all GraphQL traffic. `copy.copy(self.headers)` already carries the client default, so `if priority is not None: headers["X-Priority"] = priority.value` computes exactly `resolved = per_request if per_request is not None else client_default` (FR-006): `None` keeps the default (or absence); an explicit value overrides it, including an explicit `MEDIUM` stepping *up* from a `low` default (spec edge case, SC-003). Implementing it here means the rule exists twice (async + sync), not at every public method.
- **Alternatives considered**: a shared `_apply_priority(headers, priority)` helper — optional nicety; the two-line inline form mirrors the surrounding tracker code and is clearer in context. Left to implementer discretion; both satisfy the contract.
## Decision 5 — Threading the kwarg through higher-level methods
diff --git a/dev/specs/ihs-259-sdk-x-priority-header/spec.md b/dev/specs/ihs-259-sdk-x-priority-header/spec.md
index 108bd61fb..753690f55 100644
--- a/dev/specs/ihs-259-sdk-x-priority-header/spec.md
+++ b/dev/specs/ihs-259-sdk-x-priority-header/spec.md
@@ -12,7 +12,7 @@
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 distinguish interactive from background traffic, so under heavy background load it cannot preferentially protect the frontend.
-This feature gives the SDK a first-class notion of request **priority** (`high | normal | low`) that it emits as an `X-Priority` HTTP header. The originator of each request — the SDK caller — declares how important the request is, in a form the server can act on. Priority is set two ways: a **client-wide default** (a client dedicated to background work tags everything `low` with no call-site changes) and a **per-request override** on individual operations. When nothing is configured, the SDK sends no header and behaves exactly as today; the server treats absent/unknown values as `normal`, so rollout is safe and incremental.
+This feature gives the SDK a first-class notion of request **priority** (`high | medium | low`) that it emits as an `X-Priority` HTTP header. The originator of each request — the SDK caller — declares how important the request is, in a form the server can act on. Priority is set two ways: a **client-wide default** (a client dedicated to background work tags everything `low` with no call-site changes) and a **per-request override** on individual operations. When nothing is configured, the SDK sends no header and behaves exactly as today; the server treats absent/unknown values as `medium`, so rollout is safe and incremental.
This is the SDK's contribution to the server-side prioritization effort in INFP-636. It does **not** implement server-side admission control or 429 handling.
@@ -31,7 +31,7 @@ An operator running background workloads constructs one client with a default pr
1. **Given** a client built with a default priority of `low`, **When** it issues a GraphQL query or mutation, **Then** that request carries `X-Priority: low`.
2. **Given** a client built with a default priority of `low`, **When** it issues a multipart file upload, **Then** that request carries `X-Priority: low`.
3. **Given** a client built with a default priority of `low`, **When** it issues a raw blob transfer (`_get`/`_post`), **Then** that request carries `X-Priority: low`.
-4. **Given** a client built with a default priority of `normal`, **When** it issues any request, **Then** that request carries `X-Priority: normal` (an explicitly configured default is always emitted).
+4. **Given** a client built with a default priority of `medium`, **When** it issues any request, **Then** that request carries `X-Priority: medium` (an explicitly configured default is always emitted).
---
@@ -97,30 +97,30 @@ A developer using the synchronous client (`InfrahubClientSync`) gets behaviour i
### Edge Cases
-- **Explicit step-up on a low-default client**: a per-request `NORMAL` on a client whose default is `low` sends `X-Priority: normal` for that call — explicit intent wins, even when stepping *up* from the default.
-- **No "send no header" per-request escape once a default is set**: once a client default is configured there is no per-request way to suppress the header; the accepted equivalent is passing `NORMAL` explicitly.
+- **Explicit step-up on a low-default client**: a per-request `MEDIUM` on a client whose default is `low` sends `X-Priority: medium` for that call — explicit intent wins, even when stepping *up* from the default.
+- **No "send no header" per-request escape once a default is set**: once a client default is configured there is no per-request way to suppress the header; the accepted equivalent is passing `MEDIUM` explicitly.
- **Batch mode and raw blob transfers**: these inherit the client default but expose **no** per-call override in v1.
- **Invalid configured value**: errors at configuration load, never at request time.
- **Caller manually pre-populates an `X-Priority` header**: only reachable via the low-level `_get`/`_post` transport methods, which accept a raw `headers=` argument (the covered public methods do not). The resolution rule is the single source of truth (documented behaviour); manually injecting the header at that low level is not a supported side channel and its interaction with resolution is not guaranteed.
-- **Explicit per-request `NORMAL` vs. no argument**: an explicit `NORMAL` always emits `X-Priority: normal`; a `None`/absent per-request value falls through to the client default (which may itself be absent, in which case no header is sent).
+- **Explicit per-request `MEDIUM` vs. no argument**: an explicit `MEDIUM` always emits `X-Priority: medium`; a `None`/absent per-request value falls through to the client default (which may itself be absent, in which case no header is sent).
## Requirements *(mandatory)*
### Functional Requirements
-- **FR-001**: System MUST expose a closed set of priority values `high | normal | low` as a `Priority` enum, so callers express intent as a typed value rather than a raw header string.
+- **FR-001**: System MUST expose a closed set of priority values `high | medium | low` as a `Priority` enum, so callers express intent as a typed value rather than a raw header string.
- **FR-002**: Users MUST be able to configure a client-wide default priority via configuration, accepting either a `Priority` enum value or a case-insensitive string (through environment or file configuration).
- **FR-003**: When a default priority is configured, System MUST attach the `X-Priority` header to **every** outgoing request across all transports — GraphQL queries/mutations, multipart uploads, and raw blob `_get`/`_post`.
- **FR-004**: When no priority is configured and none is supplied per request, System MUST omit the `X-Priority` header entirely, producing outgoing requests byte-for-byte identical to current (pre-feature) behaviour.
- **FR-005**: Users MUST be able to override priority per request via a `priority` argument (accepting a `Priority` value or `None`, default `None`) on the covered public methods: `get`, `all`, `create`, `save`, the diff methods, `execute_graphql`, and its file variant.
-- **FR-006**: Priority resolution MUST be `resolved = per_request if per_request is not None else client_default`. A resolved value of `None` MUST omit the header; a resolved explicit value MUST be sent, including an explicit `NORMAL`, which MUST send `X-Priority: normal`.
+- **FR-006**: Priority resolution MUST be `resolved = per_request if per_request is not None else client_default`. A resolved value of `None` MUST omit the header; a resolved explicit value MUST be sent, including an explicit `MEDIUM`, which MUST send `X-Priority: medium`.
- **FR-007**: System MUST reject an invalid or unknown configured priority value at configuration-load time (a validation/type error) rather than coercing it or silently sending it.
- **FR-008**: The asynchronous client (`InfrahubClient`) and the synchronous client (`InfrahubClientSync`) MUST behave identically for every aspect of this feature.
-- **FR-009**: The emitted header MUST be named exactly `X-Priority`, carrying the lowercase value string (`high`, `normal`, `low`) that corresponds to the resolved priority.
+- **FR-009**: The emitted header MUST be named exactly `X-Priority`, carrying the lowercase value string (`high`, `medium`, `low`) that corresponds to the resolved priority.
### Key Entities *(include if feature involves data)*
-- **Priority** *(new)*: a closed enumeration with members `HIGH`, `NORMAL`, `LOW`. It is the single in-code representation of request priority, owned by the SDK, with no lifecycle beyond its value. Each member maps to a lowercase wire value (`high`/`normal`/`low`).
+- **Priority** *(new)*: a closed enumeration with members `HIGH`, `MEDIUM`, `LOW`. It is the single in-code representation of request priority, owned by the SDK, with no lifecycle beyond its value. Each member maps to a lowercase wire value (`high`/`medium`/`low`).
- **Configuration** *(extended)*: gains a `priority` field defaulting to `None` (meaning "no default; omit the header"). Accepts a `Priority` value or a case-insensitive string, validated at load time.
- **Base request headers** *(extended)*: the client's base header set is extended so a configured default is injected once and rides every transport, rather than being added at each call site.
@@ -137,7 +137,7 @@ A developer using the synchronous client (`InfrahubClientSync`) gets behaviour i
## Assumptions
-- **Server contract (per INFP-636)**: the header is exactly `X-Priority`; values are case-insensitive on the server; absent or unknown values are treated as `normal` server-side. This makes an absent header and a `normal` value semantically equivalent to the server, which is why omitting the header when unconfigured is safe.
+- **Server contract (per INFP-636)**: the header is exactly `X-Priority`; values are case-insensitive on the server; absent or unknown values are treated as `medium` server-side. This makes an absent header and a `medium` value semantically equivalent to the server, which is why omitting the header when unconfigured is safe.
- **Dual async/sync pattern is mandatory**: every change here is applied to both `InfrahubClient` and `InfrahubClientSync`, per AGENTS.md.
- **Public API signature change is accepted**: this feature adds a new enum, a new configuration field, and a new `priority` keyword argument across the covered public method surface — an intentional public-API-signature change (flagged per AGENTS.md "ask first: changing public API signatures").
- **Docs regeneration is required**: the new configuration field and docstrings require `uv run invoke docs-generate`.
@@ -153,4 +153,4 @@ A developer using the synchronous client (`InfrahubClientSync`) gets behaviour i
## Dependencies
-- **INFP-636** — server-side API Request Prioritization (parent effort that defines and consumes the `X-Priority` contract). This SDK feature is only useful once the server acts on the header, but is safe to ship independently because absent/unknown is treated as `normal`.
+- **INFP-636** — server-side API Request Prioritization (parent effort that defines and consumes the `X-Priority` contract). This SDK feature is only useful once the server acts on the header, but is safe to ship independently because absent/unknown is treated as `medium`.
diff --git a/dev/specs/ihs-259-sdk-x-priority-header/tasks.md b/dev/specs/ihs-259-sdk-x-priority-header/tasks.md
index c0ad370ca..dc23e30f2 100644
--- a/dev/specs/ihs-259-sdk-x-priority-header/tasks.md
+++ b/dev/specs/ihs-259-sdk-x-priority-header/tasks.md
@@ -42,9 +42,9 @@ Single-project Python library. Production code under `infrahub_sdk/`; tests unde
**Purpose**: The `Priority` enum and `Config.priority` field are prerequisites for every user story. MUST complete before Phase 3+.
-- [X] T002 Add `class Priority(str, enum.Enum)` with members `HIGH = "high"`, `NORMAL = "normal"`, `LOW = "low"` and a case-insensitive `_missing_` classmethod (returns the member matching `value.lower()`, else `None`) to `infrahub_sdk/constants.py` (mirror `InfrahubClientMode`). Add module docstring/type hints per repo style.
+- [X] T002 Add `class Priority(str, enum.Enum)` with members `HIGH = "high"`, `MEDIUM = "medium"`, `LOW = "low"` and a case-insensitive `_missing_` classmethod (returns the member matching `value.lower()`, else `None`) to `infrahub_sdk/constants.py` (mirror `InfrahubClientMode`). Add module docstring/type hints per repo style.
- [X] T003 [P] Export `Priority` from the SDK public namespace: add it to `infrahub_sdk/__init__.py` imports and `__all__` (alongside other public enums), per contracts/priority-api.md.
-- [X] T004 Add `priority: Priority | None = Field(default=None, description="Default request priority emitted as the X-Priority header on every request; one of high|normal|low (case-insensitive). When unset, no header is sent.")` to `ConfigBase` in `infrahub_sdk/config.py`; import `Priority` from `.constants`. Confirm the field auto-binds to `INFRAHUB_PRIORITY` (no custom source needed) and is carried by `Config.clone()` (it iterates `model_fields`, so no change to `clone()` required — verify only).
+- [X] T004 Add `priority: Priority | None = Field(default=None, description="Default request priority emitted as the X-Priority header on every request; one of high|medium|low (case-insensitive). When unset, no header is sent.")` to `ConfigBase` in `infrahub_sdk/config.py`; import `Priority` from `.constants`. Confirm the field auto-binds to `INFRAHUB_PRIORITY` (no custom source needed) and is carried by `Config.clone()` (it iterates `model_fields`, so no change to `clone()` required — verify only).
**Checkpoint**: `Priority` importable from `infrahub_sdk`; `Config(priority=...)` accepts enum/string; the SDK still imports and existing tests still pass.
@@ -66,7 +66,7 @@ Single-project Python library. Production code under `infrahub_sdk/`; tests unde
- [X] T007 [P] [US1] In `tests/unit/sdk/test_object_store.py` (or `test_priority.py`), assert a `priority=Priority.LOW` client emits `X-Priority: low` on a blob download (`_get_streaming`) and upload (`_post`/object-store), both clients. (SC-001, SC-006 blob)
- [X] T008 [P] [US1] Add a multipart-upload test: a `priority=Priority.LOW` client emits `X-Priority: low` on `_execute_graphql_with_file`, both clients (confirm the header survives the `content-type` pop). (SC-001, FR-003)
- [X] T009 [P] [US1] Add a batch-mode test: a `priority=Priority.LOW` client issues a batched operation and every batched request carries `X-Priority: low`. (SC-006 batch)
-- [X] T010 [P] [US1] Add a test that a `priority=Priority.NORMAL` client emits `X-Priority: normal` on requests (an explicitly configured default is always emitted, not omitted), both clients. (US1 acceptance #4, FR-006)
+- [X] T010 [P] [US1] Add a test that a `priority=Priority.MEDIUM` client emits `X-Priority: medium` on requests (an explicitly configured default is always emitted, not omitted), both clients. (US1 acceptance #4, FR-006)
**Checkpoint**: US1 fully testable and green independently — this is a shippable MVP.
@@ -110,7 +110,7 @@ Single-project Python library. Production code under `infrahub_sdk/`; tests unde
- [X] T021 [P] [US2] Test: no-default client + `priority=Priority.HIGH` on `execute_graphql` emits `X-Priority: high`; a following un-annotated call emits no header (no leak). Both clients. (SC-003, US2 acceptance #1/#2)
- [X] T022 [P] [US2] Test: `priority=Priority.LOW` default client + per-request `priority=Priority.HIGH` emits `X-Priority: high` for that call, and the next un-annotated call reverts to `X-Priority: low`. Both clients. (SC-003, US2 acceptance #3)
-- [X] T023 [P] [US2] Test: `priority=Priority.LOW` default client + per-request `priority=Priority.NORMAL` emits `X-Priority: normal` (explicit step-up wins). Both clients. (spec Edge Cases, SC-003)
+- [X] T023 [P] [US2] Test: `priority=Priority.LOW` default client + per-request `priority=Priority.MEDIUM` emits `X-Priority: medium` (explicit step-up wins). Both clients. (spec Edge Cases, SC-003)
- [X] T024 [P] [US2] Test the override on the covered surfaces: `get`, `all` (multi-page — assert every page request carries the override), `create`, `save`, a diff method, and `_execute_graphql_with_file`. Both clients. (FR-005, critique E2/E3)
**Checkpoint**: Override works and resolves correctly on every covered surface, both clients.
@@ -128,7 +128,7 @@ Single-project Python library. Production code under `infrahub_sdk/`; tests unde
### Tests
- [X] T025 [P] [US4] In `tests/unit/sdk/test_config.py`, assert `Config(address="http://localhost:8000", priority="lowe")` raises `pydantic.ValidationError` (use `pytest.raises(..., match=...)`); assert no request is issued. (SC-004, FR-007)
-- [X] T026 [P] [US4] Assert case-insensitive acceptance: `Config(priority="LOW")`, `Config(priority="Low")`, `Config(priority="low")`, and `Config(priority=Priority.LOW)` all yield `Priority.LOW`; likewise for HIGH/NORMAL. Include the env-var path `INFRAHUB_PRIORITY=LOW` via `monkeypatch`. (SC-004, FR-002)
+- [X] T026 [P] [US4] Assert case-insensitive acceptance: `Config(priority="LOW")`, `Config(priority="Low")`, `Config(priority="low")`, and `Config(priority=Priority.LOW)` all yield `Priority.LOW`; likewise for HIGH/MEDIUM. Include the env-var path `INFRAHUB_PRIORITY=LOW` via `monkeypatch`. (SC-004, FR-002)
- [X] T027 [P] [US4] Assert `Config()` default → `priority is None` (no default, header omitted). (FR-004)
**Checkpoint**: Misconfiguration fails loudly; valid config in any case is accepted.
diff --git a/docs/docs/python-sdk/reference/config.mdx b/docs/docs/python-sdk/reference/config.mdx
index cb29cc1d7..0d3b9e3f6 100644
--- a/docs/docs/python-sdk/reference/config.mdx
+++ b/docs/docs/python-sdk/reference/config.mdx
@@ -136,7 +136,7 @@ The following settings can be defined in the `Config` class
## priority
-**Description**: Default request priority emitted as the X-Priority header on every request; one of high|normal|low (case-insensitive). When unset, no header is sent.
+**Description**: Default request priority emitted as the X-Priority header on every request; one of high|medium|low (case-insensitive). When unset, no header is sent.
**Type**: `object`
**Environment variable**: `INFRAHUB_PRIORITY`
diff --git a/infrahub_sdk/config.py b/infrahub_sdk/config.py
index 73e8bd2dd..d43018bd7 100644
--- a/infrahub_sdk/config.py
+++ b/infrahub_sdk/config.py
@@ -60,7 +60,7 @@ class ConfigBase(BaseSettings):
default=None,
description=(
"Default request priority emitted as the X-Priority header on every request; "
- "one of high|normal|low (case-insensitive). When unset, no header is sent."
+ "one of high|medium|low (case-insensitive). When unset, no header is sent."
),
)
retry_delay: int = Field(default=5, description="Number of seconds to wait until attempting a retry.")
diff --git a/infrahub_sdk/constants.py b/infrahub_sdk/constants.py
index 35a19e66d..785445d9a 100644
--- a/infrahub_sdk/constants.py
+++ b/infrahub_sdk/constants.py
@@ -17,7 +17,7 @@ class Priority(str, enum.Enum):
"""
HIGH = "high"
- NORMAL = "normal"
+ MEDIUM = "medium"
LOW = "low"
@classmethod
diff --git a/tests/unit/sdk/test_config.py b/tests/unit/sdk/test_config.py
index 9a842dc7e..527411276 100644
--- a/tests/unit/sdk/test_config.py
+++ b/tests/unit/sdk/test_config.py
@@ -109,7 +109,7 @@ def test_invalid_priority_rejected() -> None:
# statically types Priority | None but coerces strings at runtime, so the dynamic input is passed
# via a dict[str, Any] rather than suppressed with a type-checker ignore.
kwargs: dict[str, Any] = {"address": "http://localhost:8000", "priority": "lowe"}
- with pytest.raises(ValidationError, match=r"Input should be 'high', 'normal' or 'low'"):
+ with pytest.raises(ValidationError, match=r"Input should be 'high', 'medium' or 'low'"):
Config(**kwargs)
@@ -125,10 +125,10 @@ class PriorityCase:
PriorityCase(name="high-title", value="High", expected=Priority.HIGH),
PriorityCase(name="high-lower", value="high", expected=Priority.HIGH),
PriorityCase(name="high-enum", value=Priority.HIGH, expected=Priority.HIGH),
- PriorityCase(name="normal-upper", value="NORMAL", expected=Priority.NORMAL),
- PriorityCase(name="normal-title", value="Normal", expected=Priority.NORMAL),
- PriorityCase(name="normal-lower", value="normal", expected=Priority.NORMAL),
- PriorityCase(name="normal-enum", value=Priority.NORMAL, expected=Priority.NORMAL),
+ PriorityCase(name="medium-upper", value="MEDIUM", expected=Priority.MEDIUM),
+ PriorityCase(name="medium-title", value="Medium", expected=Priority.MEDIUM),
+ PriorityCase(name="medium-lower", value="medium", expected=Priority.MEDIUM),
+ PriorityCase(name="medium-enum", value=Priority.MEDIUM, expected=Priority.MEDIUM),
PriorityCase(name="low-upper", value="LOW", expected=Priority.LOW),
PriorityCase(name="low-title", value="Low", expected=Priority.LOW),
PriorityCase(name="low-lower", value="low", expected=Priority.LOW),
@@ -151,7 +151,7 @@ def test_priority_case_insensitive_acceptance(case: PriorityCase) -> None:
[
pytest.param("LOW", Priority.LOW, id="low"),
pytest.param("HIGH", Priority.HIGH, id="high"),
- pytest.param("NORMAL", Priority.NORMAL, id="normal"),
+ pytest.param("MEDIUM", Priority.MEDIUM, id="medium"),
],
)
def test_priority_from_env_var(monkeypatch: pytest.MonkeyPatch, env_value: str, expected: Priority) -> None:
diff --git a/tests/unit/sdk/test_priority.py b/tests/unit/sdk/test_priority.py
index abf5935f7..2f7b9cda5 100644
--- a/tests/unit/sdk/test_priority.py
+++ b/tests/unit/sdk/test_priority.py
@@ -685,17 +685,17 @@ class ResolutionCase:
ResolutionCase(name="no-default-no-override", client_default=None, per_request=None, expected=None),
ResolutionCase(name="no-default-override-high", client_default=None, per_request=Priority.HIGH, expected="high"),
ResolutionCase(
- name="no-default-override-normal", client_default=None, per_request=Priority.NORMAL, expected="normal"
+ name="no-default-override-medium", client_default=None, per_request=Priority.MEDIUM, expected="medium"
),
ResolutionCase(name="low-default-no-override", client_default=Priority.LOW, per_request=None, expected="low"),
ResolutionCase(
name="low-default-override-high", client_default=Priority.LOW, per_request=Priority.HIGH, expected="high"
),
ResolutionCase(
- name="low-default-override-normal", client_default=Priority.LOW, per_request=Priority.NORMAL, expected="normal"
+ name="low-default-override-medium", client_default=Priority.LOW, per_request=Priority.MEDIUM, expected="medium"
),
ResolutionCase(
- name="normal-default-no-override", client_default=Priority.NORMAL, per_request=None, expected="normal"
+ name="medium-default-no-override", client_default=Priority.MEDIUM, per_request=None, expected="medium"
),
ResolutionCase(
name="high-default-override-low", client_default=Priority.HIGH, per_request=Priority.LOW, expected="low"
diff --git a/tests/unit/sdk/test_relogin_headers.py b/tests/unit/sdk/test_relogin_headers.py
index 59d9369f2..76dd913c3 100644
--- a/tests/unit/sdk/test_relogin_headers.py
+++ b/tests/unit/sdk/test_relogin_headers.py
@@ -79,7 +79,7 @@ def test_merge_request_headers_layers_delta_over_live_base(client_type: str) ->
an explicit per-request override — of a normal header or of auth itself — takes precedence.
"""
client = _build_password_client(client_type)
- client.headers["X-Priority"] = "normal"
+ client.headers["X-Priority"] = "medium"
# A delta carrying only a per-request priority override (no auth).
merged = client._merge_request_headers({"X-Priority": "high"})