Skip to content

feat(sdk): client.get_many — batched multi-kind node fetch#1108

Draft
iddocohen wants to merge 1 commit into
developfrom
ic-feat-batched-multi-kind-fetch
Draft

feat(sdk): client.get_many — batched multi-kind node fetch#1108
iddocohen wants to merge 1 commit into
developfrom
ic-feat-batched-multi-kind-fetch

Conversation

@iddocohen

@iddocohen iddocohen commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Add InfrahubClient.get_many (and the sync twin): one GraphQL
operation, one aliased block per kind, one round-trip.

result = await client.get_many({
    "InfraAutonomousSystem": {"ids": [as_id_1, as_id_2], "attributes": ["asn"]},
    "InfraDevice":           {"ids": [dev_id_1],          "attributes": ["role"]},
})
# result["InfraDevice"] -> list[InfrahubNode]

Today this shape costs N round-trips with per-kind client.filters (each pulling the full default selection set), or a hand-rolled execute_graphql with manual alias plumbing and a hand-written hydration loop. After this change, it's a single call.

Why this is needed

The pattern — read attributes off a heterogeneous set of (kind, id) tuples in one shot — recurs across the Infrahub ecosystem. Concrete domains that will adopt get_many immediately:

Domain Where the pattern shows up
Generators (infrahub-managing-generators skill, opsmill demos) Design-driven generators read a topology spec, the rack layout, IPAM allocations, vendor catalog — all heterogeneous, all needed before any synthesis. Today: a flurry of client.get / client.filters calls.
Python transforms Anything beyond ""render this one node"" needs to stitch attributes from several kinds. Today the workaround is either many round-trips or a hand-rolled execute_graphql.
Checks (other than reachability) Impact-assessment / blast-radius checks fan out from a changed node to its dependents across kinds; verdicts depend on attributes from each. Same compile-then-execute shape.
infrahub-sync / terraform-provider-generator / infrahub-ansible Bulk export / inventory build today does per-kind passes. get_many collapses a multi-kind sweep to one call.
Change-impact / branch-diff reporting The diff API returns (kind, id) tuples for changed nodes. To render a human report you need attributes per id — current options are N gets or hand-built GraphQL.
CLI tooling, dashboards, ad-hoc scripts Any ""give me a summary across these entities"" command is the same shape.
Migrations / one-shot data fixes ""For these 10k nodes across 5 kinds, read attribute X, mutate Y."" Per-kind today; batched after.

The litmus test for adoption is simple: search the opsmill org for execute_graphql( and look at what those queries do. Whenever the query is a hand-built alias-block multi-fetch (rather than a genuinely custom GraphQL operation the SDK can't express), that's a future caller of get_many. The canonical example today is _fetch_attributes in opsmill/infrahub-demo-reachability-check — that method drops from ~70 lines of query construction + hydration to ~10 lines of domain logic once get_many lands.

What ships

  • infrahub_sdk.client.compile_get_many_query — pure helper that turns a kind→{ids, attributes} spec into (query, variables, kinds_ordered). Validates kind and attribute names as GraphQL identifiers up front and collects every problem before raising ValidationError.
  • InfrahubClient.get_many / InfrahubClientSync.get_many — async and sync. Reuse execute_graphql for transport, InfrahubNode.from_graphql for hydration, and client.store.set for store population. populate_store=True matches client.get semantics.

Reuses existing primitives end-to-end. No new exception classes, no new result types, no new dependencies. The return is dict[str, list[InfrahubNode]].

Error handling

Compile-time problems (empty spec, missing/empty ids, malformed kind/attribute identifiers, scalar str/bytes passed where a list is expected) raise ValidationError with a per-problem message list before any HTTP call is made. Server-side rejections (unknown kind or attribute on the loaded schema) propagate as GraphQLError from execute_graphql.

Back-compat

Additive only. get, filters, all, traverse_paths, execute_graphql, and PathNode.fetch() are untouched.

Test plan

  • Pure-function tests over compile_get_many_query: query shape, alias allocation, variable allocation, id/attribute dedup + sort, kind-order preservation, every compile-error path, scalar str/bytes rejection for ids and attributes
  • Round-trip tests via httpx_mock parametrized over [""standard"", ""sync""]: hydration, populate_store=True/False, compile errors short-circuit before the HTTP call
  • Full SDK unit suite: 1030+ tests passing
  • uv run invoke format lint-code clean (ruff, ty, mypy)
  • Integration tests (integration-tests-latest-infrahub) passing in CI

Related issues

  • opsmill/infrahub#9067 — the same N+1 problem on the frontend (one GraphQL query per related node on the task details page). Not directly fixed by this PR (frontend is TypeScript), but the design here is the blueprint for the TypeScript port.

Follow-ups (not in this PR)

  • PathTraversalResult.fetch_hop_attributes(spec) sugar — one-liner over get_many for the reachability-check shape.
  • include_attributes= keyword on traverse_paths — sugar that calls the above before returning.

@github-actions github-actions Bot added the type/documentation Improvements or additions to documentation label Jun 28, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 28, 2026

Copy link
Copy Markdown

Deploying infrahub-sdk-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5b8047c
Status: ✅  Deploy successful!
Preview URL: https://9c30417a.infrahub-sdk-python.pages.dev
Branch Preview URL: https://ic-feat-batched-multi-kind-f.infrahub-sdk-python.pages.dev

View logs

@codecov

codecov Bot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

❗ There is a different number of reports uploaded between BASE (037de61) and HEAD (5b8047c). Click for more details.

HEAD has 3 uploads less than BASE
Flag BASE (037de61) HEAD (5b8047c)
python-3.14 1 0
python-3.13 1 0
integration-tests 1 0
@@             Coverage Diff             @@
##           develop    #1108      +/-   ##
===========================================
- Coverage    82.15%   75.40%   -6.76%     
===========================================
  Files          138      138              
  Lines        11897    11965      +68     
  Branches      1784     1798      +14     
===========================================
- Hits          9774     9022     -752     
- Misses        1575     2399     +824     
+ Partials       548      544       -4     
Flag Coverage Δ
integration-tests ?
python-3.10 55.47% <91.42%> (+0.22%) ⬆️
python-3.11 55.48% <91.42%> (+0.22%) ⬆️
python-3.12 55.48% <91.42%> (+0.23%) ⬆️
python-3.13 ?
python-3.14 ?
python-filler-3.12 22.65% <8.57%> (-0.10%) ⬇️

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

Files with missing lines Coverage Δ
infrahub_sdk/client.py 71.33% <100.00%> (-4.22%) ⬇️

... and 34 files with indirect coverage changes

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 5 files

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

Re-trigger cubic

Comment thread infrahub_sdk/client.py
Comment thread infrahub_sdk/client.py Outdated
@iddocohen iddocohen changed the base branch from stable to develop June 28, 2026 14:03
@iddocohen iddocohen marked this pull request as ready for review June 28, 2026 14:05
@iddocohen iddocohen requested a review from a team as a code owner June 28, 2026 14:05
iddocohen added a commit that referenced this pull request Jun 28, 2026
`{"InfraDevice": {"ids": "dev-1"}}` and
`{"InfraDevice": {"ids": ["dev-1"], "attributes": "name"}}` are easy
mistakes — a caller with one id or one attribute reaches for the bare
value rather than wrapping it in a list. Strings and bytes are
iterable in Python, so the compiler used to walk them
character-by-character: the first form sent seven one-character
GraphQL ids, and the second built a selection set of
`n { value } a { value } m { value } e { value }` because each
character passed the GraphQL-identifier regex.

Reject `str`/`bytes` explicitly for both fields and surface a
descriptive `ValidationError` instead. The original list-of-id /
list-of-attribute path is unchanged.

Reported by the cubic AI code reviewer on #1108.
Adds InfrahubClient.get_many (and the sync twin) plus the
compile_get_many_query helper. Callers pass a kind -> {ids, attributes}
spec and get back one GraphQL operation with one aliased block per kind
(k0, k1, ...) and one [ID] variable per kind (ids_0, ids_1, ...). The
cost is a single round-trip regardless of how many kinds are in the
spec — the alternative today is one client.filters call per kind, or a
hand-rolled execute_graphql with manual alias plumbing.

The compile helper validates kind and attribute names as GraphQL
identifiers up front and raises ValidationError listing every problem
before any HTTP call is made. Scalar str/bytes passed where a list is
expected for ids or attributes are rejected explicitly — Python's
iteration over a bare string would otherwise turn one id or attribute
into N one-character entries, silently producing an invalid query.
Server-side rejections still propagate as GraphQLError. Hydration
reuses InfrahubNode.from_graphql so callers get typed attribute
access; populate_store=True mirrors client.get.

Reuses existing primitives end-to-end:
  - execute_graphql for the network call
  - InfrahubNode.from_graphql / InfrahubNodeSync.from_graphql for hydration
  - client.store.set for store population
  - ValidationError / GraphQLError from infrahub_sdk.exceptions

No new exception classes, no new result types, no new dependencies.
The return is dict[str, list[InfrahubNode]].
@iddocohen iddocohen force-pushed the ic-feat-batched-multi-kind-fetch branch from 1db243e to 5b8047c Compare June 28, 2026 14:33
@iddocohen iddocohen added the type/feature New feature or request label Jun 28, 2026

@ogenstad ogenstad 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.

As mentioned in Slack I think we should have an extra round of design for this before moving forward.

@iddocohen iddocohen marked this pull request as draft June 29, 2026 08:56
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