Repository navigation
v1.24.0b0
Pre-release1.24.0b0 - 2026-11-02
Removed
-
Removed
client.branch.diff_data()from both the async and sync clients. The method relied on aGET /api/diff/dataREST endpoint that does not exist in Infrahub, so every call returned a 404. Useclient.get_diff_tree()to retrieve the full diff of a branch against its base branch, orclient.get_diff_summary()for the list of changed nodes; both use theDiffTreeGraphQL query. (#325) -
numpyandmdxifyare no longer installed by thectlandallextras.Neither is used by the SDK.
numpywas only ever needed because olderpyarrowreleases required it, andpyarrowstill installs it itself on the versions that do.mdxifyis a documentation tool used when building these docs, not at runtime.If your project imports either package directly, add it to your own dependencies rather than relying on
infrahub-sdk[ctl]to supply it.
Added
-
Added the Infrahub deployment ID to the
infrahubctl infocommand output and aget_server_information()method (returning the server version and deployment ID) on the async and sync clients. (#1017) -
Added transparent retry of HTTP 429 (rate-limited) responses on both
InfrahubClientandInfrahubClientSync. Retries use jittered exponential backoff and honour a server-providedRetry-Afterheader (delta-seconds or HTTP-date). The behaviour is tunable through four newConfigfields (rate_limit_retry_enabled,rate_limit_max_retries,rate_limit_backoff_base,rate_limit_backoff_max), and a newRateLimitErrorexception is raised when retries are exhausted. (#1124) -
Added support for tagging SDK requests with a priority via a new
X-Priorityheader. APriorityenum (high,medium,low) is available frominfrahub_sdk.constants; setConfig.priority(env varINFRAHUB_PRIORITY) for a client-wide default emitted on every request, or passpriority=to individual operations to override it per request. When unset, no header is sent. Works identically onInfrahubClientandInfrahubClientSync. (#1151) -
A
CoreNumberPoolcan be created with its ranges in one call, by passingrangesas a list of dictionaries withstart,endandallocation_weight, any of which can be left out. Saving withallow_upsert=Truesets the ranges of an existing pool to the declared list. Before, the SDK sent each range as an empty object, which Infrahub read as one range without a start or an end. A single range of an existing pool is still created, changed or deleted through itsCoreNumberPoolRangenode. Requires Infrahub 1.12 or later. -
Add
infrahubctl schema formatcommand, 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 byorder_weight(--sort-by-order-weight), and backfill a missingorder_weight(--backfill-order-weight). -
Add the
connect_timeoutsetting (INFRAHUB_CONNECT_TIMEOUT, default 10 seconds) so the TCP/TLS connection phase of every request is bounded separately from the requesttimeout. An unreachable address now fails fast instead of consuming the whole request budget, which lets endpoints published through DNS round robin fall over to the next address quickly. -
Add the
infrahubctl troubleshooting trace-sample-ratiocommands to read, set and clear the temporary trace sampling ratio of an Infrahub instance. -
Added
retry()andcancel()methods to the task manager. TheTaskmodel now exposesavailable_actionsalong withcan_retry/can_cancelhelpers. -
Added an opt-in
include_diagnosticsflag to the task manager'sall(),filter(), andget()methods. When enabled, tasks expose anerrorfield, andwebhook-sendtasks are returned asWebhookDeliveryTaskinstances carryinghttp_request/http_responsedelivery details. -
Added support for Python 3.15. The
requires-pythonrange now allows 3.15, and the unit tests run on 3.15 in CI. -
Added support for the new
IPAddressattribute kind. Values are exposed as bareipaddress.IPv4Address/IPv6Addressobjects (no prefix) and serialized to a bare-address string when writing, alongside the existingIPHostandIPNetworkkinds. -
Added the
onlyargument toget,all,filtersand relationshipfetch, to fetch only the named attributes and relationships; reading a field that was not fetched raisesFieldNotLoadedError, andis_loadedtells whether a field was fetched. -
Added the location, the received value and the reason as separate fields (
loc,input,reason) on every schema validation error, next to the unchangedfieldandmessage, so a consumer can report each problem without parsing the message text. -
Codes in Infrahub's error catalogue now raise an exception class of their own, importable from
infrahub_sdk.exceptions, carrying the failure's payload as directly typed attributes. Identifying a specific failure no longer means matching words in a message:from infrahub_sdk.exceptions import ApiError, UniquenessViolationError try: await node.save() except UniquenessViolationError as exc: print(exc.node_kind, exc.fields) # "TestPerson", ["name"] except ApiError as exc: print("some other failure:", exc.code)
The new classes are
AttributeConstraintViolationError,AttributeInvalidTypeError,AttributeRequiredError,BranchAlreadyMergedError,BranchNeedsRebaseError,MergeInProgressError,MergeRecoveryRequiredError,UndefinedError, andUniquenessViolationError. Each is typed exactly as the catalogue declares the payload, so a required field is never optional and needs no guard. They all descend fromGraphQLError, so noexceptclause stops catching what it catches today.NODE_NOT_FOUND,BRANCH_NOT_FOUNDandSCHEMA_NOT_FOUNDreach the classes the SDK already had for them, listed under Changed.exc.http_statusreads the status the envelope declares. Where the envelope omits one, a generated class supplies the status the catalogue gives its code; the three lookup-miss classes declare none, since they are also raised with no server behind them, and stayNonethere.AUTHENTICATION_REQUIRED,TOKEN_EXPIRED, andPERMISSION_DENIEDdeliberately have no class of their own: each of them reaches the SDK on two transports, and which generic class it raises follows the transport the SDK observed rather than the status the code declares. CatchApiErrorand testexc.codeto handle one of the three whichever way it arrived.A server predating a code, or one whose payload does not match what the catalogue declares for it, still raises the generic class for the transport with
exc.codereadable, so an SDK of any version keeps working against a server of any version.docs/python-sdk/topics/error_handlingcovers the hierarchy, the attributes readable on a caught error, and the cross-version guarantees. -
Number attributes have
attach_to_pool()anddetach_from_pool().attach_to_pool()makes a number pool track the number the attribute holds, without allocating a new one, anddetach_from_pool()makes the pool stop tracking it while the attribute keeps its number. Both take effect when the node is saved, and require Infrahub 1.12 or later. -
Resource pool nodes have
get_pool_allocations(), which lists the resources a pool holds and, for a number pool, whether the pool allocated each number or a user attached it. Requires Infrahub 1.12 or later.get_pool_allocated_resources()is unchanged. -
The JSON schema generated for
.infrahub.ymlnow 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 nowatchblock is flagged, and so is awatchvalue that is not a mapping, including the barewatch:that parses as null and records nothing. Both warnings are advisory only: the models still accept every one of those forms. An emptywatch: {}orfiles: []stays clean, since either one records that the author checked and nothing beyond what Infrahub detects needs watching. The generated schema also picks up thewatchblock on generator definitions, which it was previously rejecting as an unknown property. -
The SDK sends the W3C
traceparentheader with each request to Infrahub when OpenTelemetry is installed and a span is active, so the Infrahub server records its spans in the caller's trace. The newtracingextra installs the OpenTelemetry API. -
The request priority (
X-Priorityheader) can now be carried on the client'sRequestContextvia a newpriorityfield, alongside the existing client-wideConfig.prioritydefault and per-callpriority=override. Resolution precedence is per-callpriority=>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 onInfrahubClientandInfrahubClientSync. -
The schema models accept
parameters.rangeson NumberPool attributes, a list of ranges with an optionalstart,endandweight; a range without astartor anendis open on that side. Thestart_range/end_rangeshorthand is now optional and deprecated.rangesisNonewhen a NumberPool attribute declares no range, which Infrahub reads as one range without a start or an end, and an empty list declares no range. -
queryandvariablesare now readable on everyApiError, not only on the GraphQL branch. Reading either off anAuthenticationErrorpreviously raisedAttributeError:try: await client.execute_graphql(query=query) except ApiError as exc: log.error("request failed", code=exc.code, query=exc.query) # no longer raises on a 401
That clause is the one the SDK recommends for the three authentication codes, since each of them can arrive either on a real 401 or 403 or inside a GraphQL
errorsarray, so it is exactly where the attributes had to exist. They joincode,http_status,extensionsanderrors, which were already declared there for the same reason.Nonemeans the failed request was not recorded on the exception rather than that there was no query: an authentication failure is observed at the transport, which has neither to hand. Nothing that reads these attributes today changes -GraphQLErrorstill populates both from the request it was raised for.
Changed
-
InfrahubGroupContext.delete_unused()and its sync counterpart no longer raise. They attempt every unused member and return aReapResult, importable frominfrahub_sdk.query_groups, that lists the members the server refused to delete, the members the cleanup never reached, and the failure that stopped it, if any. A caller that invokeddelete_unused()directly and relied on it raising must now inspect that result.update_group()and the tracking context manager still raise, as described below.Leaving a tracking context whose cleanup was refused now raises
TrackingGroupCleanupErroronce every member has been attempted and the tracking group has been written, in place of the firstGraphQLError. If the group write itself fails, its error is raised instead.TrackingGroupCleanupErroris not aGraphQLError, so a caller that caught the cleanup failure withexcept GraphQLErrormust catch it explicitly. A cleanup stopped by a failure that is not about a member, such as a timeout, still raises that failure itself. (#572) -
Generators, transforms and checks run without an explicit branch now target the configured
default_branch(INFRAHUB_DEFAULT_BRANCH,mainby default) instead of the active branch of the local Git checkout. If you runinfrahubctl transform,generatororchecklocally and relied on them following your Git branch, setINFRAHUB_DEFAULT_BRANCH_FROM_GIT=trueor pass--branchexplicitly. (#1290) -
A failure the server's error catalogue describes now carries a message naming that code and the server's own words, in place of the query text.
GraphQLErrorpreviously rendered asAn error occurred while executing the GraphQL Query <query>, <errors>and now readsUNIQUENESS_VIOLATION: Node of kind TestPerson already has name 'John';AuthenticationErrorreadsAUTHENTICATION_REQUIRED: <reason>. On both transports the code names the first error only, which is the one that determines the exception raised; the complete list stays onexc.errors, and the query and variables stay onexc.queryandexc.variables.A failure the catalogue does not describe keeps today's message exactly, query text and full error list included, as does one of the lookup-miss classes raised without a server behind it. That covers a server predating the catalogue, an error carrying no
extensions, and, since a current server codes every error it reports, anything the server codedUNDEFINED_ERROR.exc.codeis readable in every case, so it is not the test for which message form you are holding:code_names_the_failure(exc.code), importable frominfrahub_sdk.exceptions, is.In
infrahubctl, a described failure is now reported by its code and message rather than prefixed withAuthentication failure:or rendered as a bare error list. An undescribed one renders exactly as before, so a GraphQL validation error still shows the line and column it failed on. Code matching on any of these strings should branch onexc.codeinstead. -
A lookup miss the server reports -
NODE_NOT_FOUND,BRANCH_NOT_FOUNDorSCHEMA_NOT_FOUND- now raisesNodeNotFoundError,BranchNotFoundErrororSchemaNotFoundErrorbuilt from the payload the server sent, where it previously raised a genericGraphQLError:try: await client.delete(kind="NetworkDevice", id=device_id) except NodeNotFoundError: ... # already gone
These three are the only catalogued classes the SDK also raises on its own, for a lookup that returned nothing and for the REST 404 behind a missing file. A ladder that handles one of them specifically will now see server-reported failures arrive there alongside the SDK's own, and
exc.code is not Nonetells the two apart. -
Authentication failures now surface the server's error envelope.
AuthenticationErrorandGraphQLErrorshare a newApiErrorbase carryingcode,http_status,extensions, anderrors, so a caller can branch on the server's catalogue code instead of matching on message text.A 401 or 403 whose body the SDK cannot read as an error envelope now raises
AuthenticationErrorcarrying the best reason available: the REST API's baredetailstring where the body has one, and otherwise the plain status. Previously the same responses raisedJsonDecodeError(or, from the object store and file handler, a rawjson.JSONDecodeError) when the body was not JSON, andTypeErrorwhen the body carried anerrorsarray whose entries had nomessage. A body that was JSON but carried noerrorskey raisedAuthenticationErrorwith its generic default message, dropping the status the server sent. Code that catches those types aroundobject_store,file_handler, or a client request should catchAuthenticationErrorinstead.from infrahub_sdk.exceptions import *now yields exactly the exception classes. It previously also carried whatever the module imported for its own annotations, such asMappingandAny. Every exception class keeps its name and its import path. -
Changed
number_pool_idin the parameters of a NumberPool attribute to a read-only field: schema validation now warns that the value is ignored instead of accepting it. -
Generated protocols are now ordered by the class name they render rather than by the schema name without its namespace, so
BuiltinIPPrefixno longer sorts underIPPrefix. Regenerating withinfrahubctl protocolsreorders the classes in your file and changes nothing else. The ordering is also total now. Kinds that the old key compared equal, two sharing a name and differing only in namespace, or two whose names differ only in case, previously fell back to whatever order the schema was supplied in, which could make the same schema render differently from one run to the next. -
Paginated queries generated by
all(),filters(),get()and resource pool allocation lookups now passoffsetandlimitas 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_dataalso accepts variable placeholder strings (for example"$offset") for itsoffsetandlimitarguments. -
Raised the
richrequirement from>=12to>=12.6, the first release that honoursFORCE_COLOR. Earlier releases ignore the variable, so the test suite's rendering guard could not describe them; nothing in the SDK itself changes. -
Raised the default
Config.rate_limit_max_retriesfrom 5 to 10, so a request shed with HTTP 429 keeps retrying (honouringRetry-After) for longer before raisingRateLimitError. 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
richrequirement is now>=12.6with no upper bound, so the SDK can be installed alongside rich 14 and later.The previous
<14cap would otherwise have applied to every install rather than only those using theinfrahubctlCLI, and would have conflicted with any project already on a newer rich. The SDK's unit tests pass against rich 13, 14 and 15. -
The hand-maintained schema models in
infrahub_sdk.schemaare 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.STRINGhas been removed. It was deprecated andkind="String"was already rejected server-side; useAttributeKind.TEXTinstead.- 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_countdefault to0(wasNone), nodebranchdefaults to"aware",generate_profiledefaults toTrue, andgenerate_templatedefaults toFalse. This changes the round-trip output of programmatically-built schemas.
Constructing
AttributeSchema(name=..., kind=AttributeKind.TEXT, ...),NodeSchema,GenericSchema,RelationshipSchema,SchemaRoot, and the read-side*APImodels continues to work unchanged. -
The protocols shipped in
infrahub_sdk.protocolsnow carry the peer type of every relationship, sonode.rel.peerandnode.many_rel.peers[0].peerresolve to the peer's protocol instead of a bareInfrahubNode, and a cardinality-one relationship can be assigned an id, an HFID, a peer node orNone. They are generated by the same renderer asinfrahubctl protocolsrather than by a separate template, which also adds the relationships the API exposes implicitly (member_of_groups,subscriber_of_groups,profiles) and theProfileBuiltinIPAddress,ProfileBuiltinIPPrefix,ProfileBuiltinTagandProfileIpamNamespacekinds. Attributes that always hold a value are no longer typed as optional, and each attribute now follows its schema kind, soCoreGlobalPermission.decisionandCoreObjectPermission.decisionareIntegerrather thanEnum. Two relationships that the API never exposed,CoreGenericAccount.tokensandCoreGenericAccount.external_identities, are gone. Protocols generated for your own schema now import the fourProfile*kinds above instead of redefining them. -
Two extras now cover testing, replacing the
testsextra.pip install 'infrahub-sdk[testing]'installs pytest and enables the bundledpytest-infrahubplugin, which is what you want to test your own Transforms, Queries and Checks.pip install 'infrahub-sdk[testcontainers]'additionally providesinfrahub_sdk.testing.docker, which starts a real Infrahub in containers so your tests can run against a live instance instead of mocked responses.They are kept apart because the container tooling is a much heavier install.
[testing]adds four packages on top of a plain install;[testcontainers]adds around sixty, among them Docker, FastAPI and Prefect client libraries. Previously you would have had to take all of it to get either.The
testsextra is gone. It stopped installing anything in 1.16.0 while the documentation carried on advertising it, so since thenpip install 'infrahub-sdk[tests]'has warned about an unknown extra and installed only the base package. If you are coming from 1.15.2 or earlier,[testing]is the direct replacement for what[tests]gave you. -
NodeNotFoundError.identifieris now annotatedMapping[str, list[str]] | str. The SDK already raised it with a plain string to name a missing file, so this documents behaviour that was always there; no runtime behaviour changes and no existing caller needs updating. -
NodeNotFoundError,BranchNotFoundError, andSchemaNotFoundErrornow descend fromGraphQLError, so that one class covers a lookup miss however it arose: reported by the server, decided by the SDK, or turned from a REST 404.An
except GraphQLErrorclause therefore also catches lookup misses that involved no GraphQL request at all. Code that relied on those escaping such a clause should catch the specific class ahead of it, as an orderedexceptladder already must. Each class keeps its name, its constructor, and the message it produces when no server reported the failure, anderrors,query, andvariablesare now readable on every one of them rather than missing on a client-side raise.NodeInvalidErrorinherits the re-rooting but not the adopted code: it means a node of the wrong kind rather than a lookup miss, soNodeInvalidError.CODEisNonewhereNodeNotFoundError.CODEisNODE_NOT_FOUND. -
ObjectFile.validate_content()andMenuFile.validate_content(), theirspecproperties, andInfrahubFile.validate_content()now raiseinfrahub_sdk.exceptions.ValidationErrorwhen a file has one of these problems:- a
kindof the other file type, such askind: Menuin an object file, which previously raisedValueError - an unknown
kindorapiVersion, or a missingspec, which previously raised a pydanticValidationError - a
specthat does not match the object or menu format, which previously raised a pydanticValidationErrorfor menu files
The exception's
identifieris the file location.messagesholds one line per problem, such askind | Input should be 'Menu' or 'Object', received 'Schema' (enum). The line names the field and includes the wrong value when that value is a single string, number or boolean.infrahubctl object load,object validate,menu loadandmenu validatetherefore report these problems as a validation error for the file instead of stopping with a Python traceback.infrahubctlalso no longer drops text in square brackets from these messages.The SDK
ValidationErrordoes not subclassValueErroror the pydanticValidationError. Code that catches either of those around these methods should catchinfrahub_sdk.exceptions.ValidationErrorinstead. - a
-
infrahub-sdknow requirespydantic>=2.0.3, up from>=2.0.0.If you pin
pydanticto 2.0, 2.0.1 or 2.0.2, installing the SDK now fails while resolving dependencies. Those versions never actually worked:import infrahub_sdkraised aSchemaErroron them, because their regex engine rejects a pattern used by the schema models. Pinpydantic>=2.0.3to resolve it.anyioandtyping-extensionsare now installed as direct requirements of the SDK itself. It has always imported them but relied on other packages to pull them in, so a minimal or heavily constrained environment could end up with the SDK installed and unusable. No action is needed, installs simply become reliable.packagingis now a direct requirement of thetestcontainersextra, which is where theinfrahub_sdk.testing.dockerhelper that imports it lives. A plainpip install infrahub-sdkdoes not install it.
Fixed
-
Tracking groups now reconcile correctly when a run saves no nodes at all. Previously
update_group()returned early on an empty member list, so a generator that produced nothing (a decommissioning run) or a repository whose last object file was removed left every previously tracked node behind as an orphan, still listed in the group. Withdelete_unused_nodes=True, as generators and repository imports use it, a run that tracks nothing but has an existing group now prunes it; a run that tracks nothing and has no group still creates none. Withdelete_unused_nodes=Falsethe existing group is not read, so a run that tracks nothing still leaves it unchanged.The cleanup now deletes unused members on the branch the tracking context targets. It previously deleted them on the client's default branch, so on any other branch it removed the wrong node or found nothing to remove.
Cleanup is also no longer aborted by a single refused delete.
delete_unused()attempts every unused member and reports the refusals together asTrackingGroupCleanupErrorinstead of propagating the first one and silently skipping the rest. Members that could not be deleted are kept in the tracking group so a later run retries them. A refusal that persists, such as a node outside the run that still points at the member through a mandatory relationship, is raised again on every run until whatever blocks the deletion is removed; previously the member dropped out of the group after the first failure and stayed behind unnoticed.A failure that is about the request rather than the member being deleted, such as a timeout, an expired token, or a branch that is gone or locked by a merge, stops the cleanup and is raised as itself rather than recorded against every remaining member in turn. The tracking group write is still attempted before that failure surfaces, listing the members the cleanup never reached along with the nodes the run created, so an interrupted cleanup no longer leaves those nodes in no group at all whenever the server can still accept the write. The member whose delete was interrupted stays in the group when the server answered, since a rejected delete removed nothing. When no answer came back, such as on a timeout, the delete may have gone through, so that member is left out of the group; if the delete in fact did not happen, it stays behind as before. When the write fails as well, usually for the same reason, the failure that stopped the cleanup is the one raised, with the write's failure as its cause.
infrahubctlrendersTrackingGroupCleanupErroras a table of each member and the server's reason instead of a traceback, and the exception can be serialized so it survives a task orchestrator's failure handling. (#572) -
Sync protocols generated by
infrahubctl protocolsnow inherit the*Syncvariant of every base class that has one, instead of onlyCoreNode,CoreProfileandCoreObjectTemplate. A class inheriting another core kind (for exampleBuiltinIPAddressorLineageSource) previously kept the async base in its sync output, so itssave,deleteandcreatemethods were typed as async and a correct sync call was reported as a missingawait. Regenerate withinfrahubctl protocols: sync classes that inherited an async base will now expose the sync methods, which may surface new type errors in code that relied on the old async signatures. (#1276) -
Fixed
infrahubctl generator, which exited withThe node doesn't have an attribute for nameinstead of running when the target group contained members of a kind without anameattribute. The generator definition parameters are now resolved the same way Infrahub resolves them when it runs a generator, so those targets work and every declared parameter reaches the query instead of only the first one. (#1289) -
Fixed
infrahubctl generator,transformandcheckfailing withBranchNotFoundErrorwhen run without--branchwhile the active local Git branch did not exist in Infrahub. (#1290) -
Fixed a requested branch being replaced by the local Git branch when
default_branch_from_gitis enabled. This affectedclient.clone(branch=...), generators and transforms created withbranch=..., and the--branchoption ofinfrahubctl. Calls that did not pass a branch went to the Git branch, so a generator run with--branchcould read from the requested branch and create or update nodes on the Git branch. (#1295) -
Corrected the return type annotations of
rebase()andvalidate()on bothInfrahubClient.branchandInfrahubClientSync.branch. They were declared as-> BranchDatabut have always returned the mutation'sokboolean, so the annotations now read-> boolto match the actual behaviour and align withmerge(). (#1308) -
Fixed
get_diff_summary()reporting its request under the GraphQLoperationNameGetDiffTree, the same name used byget_diff_tree(). The summary query now sendsGetDiffSummary, so the two methods are distinguishable in server-side query logs, traces, and observability tooling keyed onoperationName. (#1313) -
Fixed
fetch()raisingValueErroron theancestorsanddescendantsof a node that was retrieved without them, on both the async and the sync client. (#1425) -
A
from_poolrequest is sent once: after a successful save, the next update no longer sends it again. -
A client authenticating with an API token no longer replays a request after a 401 that reports an expired token. Only a client configured with a username and password can obtain a new token, so on the request paths that retry automatically the retry sent the same rejected token a second time, doubling the cost of the failure. Streaming downloads never retried and are unaffected.
-
A file download answered with HTTP 404 now raises
NodeNotFoundErrorwhatever the response body contains. Previously a body that was not a JSON object - an HTML error page from an intermediary, an empty body, or a JSON array - escaped as a rawjson.JSONDecodeErrororAttributeErrorinstead. -
An attribute created with both
valueandfrom_poolnow sends both, so the number pool tracks the given number instead of allocating a new one. -
Fixed
fetch()on a cardinality-many relationship that was not loaded, such aschildren, replacing the node in the client store with a copy that has no attribute values, on both the async and the sync client.fetch()on a node that has not been saved now raisesUninitializedErrorinstead ofValueError: At least one filter must be provided to get(); code that catchesValueErrorfor this case must catchUninitializedErrorinstead. -
Fixed an
AttributeErrorescaping the client when a 401 response carried a body that was valid JSON but not an object, such as the bare array or string a proxy or gateway may return. The silent token refresh now treats any body it cannot read as an envelope as carrying no refresh signal, and the request surfacesAuthenticationErroras it does for every other unreadable 401. -
Fixed peers given as
RelatedNodeobjects, such as a peer taken from another node's relationship, being stored with theRelatedNodeobject as their ID, which made the save request invalid and maderemove()remove nothing. This affected lists of peers for a cardinality-many relationship,add()andremove()on a cardinality-many relationship, and assignment to a cardinality-one relationship. -
Import
pyarrowlazily in the line-delimited JSON importer so thatinfrahubctlcommands other thanobject loadno longer require thectlextra (and its heavypyarrowdependency) to be installed. -
Parts of the SDK no longer fail to import on a plain
pip install infrahub-sdk.infrahub_sdk.template,infrahub_sdk.yaml,infrahub_sdk.spec,infrahub_sdk.transferandinfrahub_sdk.protocols_generatorimport Jinja2, PyYAML and rich, but those three were only installed by thectlextra. Using any of them without that extra raisedModuleNotFoundError, even though none of it is CLI-specific:templaterenders your Transforms, and rich supplies the traceback types carried in Jinja error reporting.All three are now installed with the SDK itself, so a plain install grows from 19 to 26 packages. The
ctlextra is correspondingly smaller and now covers only whatinfrahubctlgenuinely needs. -
Protocols generated for a hierarchical kind no longer declare
parentandchildrentwice. When the schema already exposes them as relationships, which is the case for any schema read from the API, they were emitted once from the relationship list and once more from the hierarchy, and the second pair overrode the first. Regenerate withinfrahubctl protocolsto drop the duplicates. -
Two declared lower bounds were wrong and have been corrected.
typernow requires>=0.16.0, up from>=0.15.0. Combined with theclick>=8.3the SDK already required, typer 0.15 crashed on anyinfrahubctl --helpwithTypeError: Parameter.make_metavar() missing 1 required positional argument, because click 8.3 changed that signature and typer only adapted in 0.16. The old floor advertised a combination that could not work.Jinja2now requires>=3.1.5, up from>=3. On 3.1.4 and earlier, template error reporting points at the wrong template when a nested template uses an undefined variable, and omits the source path when an imported template is missing.If you pin either package below its new floor, installing the SDK now fails while resolving instead of breaking once you run it.
-
GraphQLErrorand every exception under it now survivepickleandcopy.deepcopyintact. An exception that crosses a process boundary - a task queue, a parallel test runner, a process pool - previously came back wrong in one of two ways, because Python rebuilds an exception by calling its class with the message as a lone positional argument.GraphQLErrortakes the server's error list first, so that call filed the message undererrorsand the message a caller read came back as the placeholder built from it:An error occurred while executing the GraphQL Query None, UNIQUENESS_VIOLATION: ...in place of the real one. The per-code classes take their payload fields as required keyword arguments, so the same call raisedTypeError: __init__() takes 1 positional argument but 2 were givenand the original failure was lost entirely.Both are fixed at the root of the GraphQL branch, so the type, the message, the payload attributes,
code,http_status,extensions,errors,queryandvariablesall come back as they went in, and anexceptclause still catches a restored exception by its own class.NodeNotFoundError,BranchNotFoundErrorandSchemaNotFoundErrorwere affected more quietly: their attributes andstr()came back correct, butexc.argswas rebuilt as the class's own default sentence rather than the message the exception was raised with.AuthenticationError, whose constructor takes the message first, was the only one unaffected. -
infrahubctlno longer deletes bracketed text from an error message. A message such asThe requested branch was not found on the server [main]was printed without the branch name, because rich read the brackets as a style tag. Everything the shared error handler prints is now escaped, including the traceback the fallback branch emits. Affects branch, schema, and node lookup misses, authentication failures, HTTP transport failures, and the fallback handler. -
infrahubctlno longer exits non-zero with no output when a GraphQL failure arrives in a shape the SDK cannot read as an error envelope.infrahubctl run,infrahubctl validate graphql-query, and every command wrapped by the shared error handler now fall back to printing the exception's message, which keeps the server's payload verbatim.
Housekeeping
- Added two CI jobs covering the declared dependency ranges, which the existing test matrix never exercised because it installs exactly what
uv.lockpins. One re-resolves at the declared lower bounds and runs the unit tests; the other builds the wheel and confirms it installs, imports and runs the CLI on every supported Python with a fresh resolution. Upper bounds are tracked separately, since any upstream release could otherwise turn an unrelated pull request red. - Added unit tests over the packaging metadata so the declared dependencies stay honest. They check that every requirement has a lower bound, that the
allextra keeps covering the other extras, and that the packages the SDK imports and the packages it declares match each other. - Pinned Rich's colour and width for the test suite from
pytest_configure, so CLI-output assertions render identically regardless of the developer'sFORCE_COLOR/COLUMNSenvironment. - Routed the two remaining
existing_group.members.peer_idsaccesses ininfrahub_sdk/query_groups.pythrough the typed_get_relationship_many(name="members")accessor, removing the last two# type: ignore[union-attr]suppressions in that module. Internal typing hygiene with no behavioural change.