---
title: node
sidebarTitle: node
---

# `infrahub_sdk.node.node`

## Classes

### `InfrahubNode`

Asynchronous Infrahub node bound to an :class:`InfrahubClient`.

Provides full CRUD against the backend (:meth:`save`, :meth:`create`, :meth:`update`,
:meth:`delete`) along with relationship traversal (:meth:`get_flat_value`,
:meth:`extract`), feature-gated artifact and resource-pool helpers
(:meth:`artifact_generate`, :meth:`get_pool_allocated_resources`), and file upload
or download for nodes inheriting from ``CoreFileObject``.

Attributes and relationships defined on the schema are exposed as instance
attributes via attribute-style access (``node.name.value``, ``node.parent``).

**Methods:**

#### `from_graphql`

```python
from_graphql(cls, client: InfrahubClient, branch: str, data: dict, schema: MainSchemaTypesAPI | None = None, timeout: int | None = None) -> Self
```

Build an :class:`InfrahubNode` from a raw GraphQL response.

When no ``schema`` is provided, the node kind is read from ``__typename`` in the
payload and the schema is fetched from the client.

**Args:**

- `client`: The client used to interact with the backend.
- `branch`: The branch the node belongs to.
- `data`: The GraphQL payload describing the node.
- `schema`: Pre-fetched schema for the node kind.
Skips the schema lookup when provided.
- `timeout`: Overrides the default timeout used when fetching the
schema. Specified in seconds.

**Returns:**

- The hydrated node instance.

**Raises:**

- `ValueError`: If ``__typename`` is missing from ``data`` and no ``schema`` was provided.

#### `generate`

```python
generate(self, nodes: list[str] | None = None) -> None
```

Trigger artifact generation for this artifact definition.

Only available on nodes whose kind is ``CoreArtifactDefinition``.

**Args:**

- `nodes`: The IDs of target nodes to generate artifacts
for. When omitted, generation runs for all targets matched by the
definition.

**Raises:**

- `FeatureNotSupportedError`: If this node is not a ``CoreArtifactDefinition``.

#### `artifact_generate`

```python
artifact_generate(self, name: str) -> None
```

Regenerate a named artifact targeting this node.

Looks up the ``CoreArtifact`` named ``name`` for this node, then calls
:meth:`generate` on the related definition with this artifact's ID.

**Args:**

- `name`: The name of the artifact to regenerate.

**Raises:**

- `FeatureNotSupportedError`: If this node does not inherit from ``CoreArtifactTarget``.

#### `artifact_fetch`

```python
artifact_fetch(self, name: str) -> str | dict[str, Any]
```

Fetch the stored content of a named artifact for this node.

**Args:**

- `name`: The name of the artifact to fetch.

**Returns:**

- str | dict\[str, Any]: The artifact content. Returns a parsed object for
- JSON-typed artifacts and a string for text-typed artifacts.

**Raises:**

- `FeatureNotSupportedError`: If this node does not inherit from ``CoreArtifactTarget``.

#### `download_file`

```python
download_file(self, dest: None = None, skip_if_unchanged: bool = ...) -> bytes
```

<details>
<summary>Show 2 other overloads</summary>

#### `download_file`

```python
download_file(self, dest: Path, skip_if_unchanged: bool = ...) -> int
```

#### `download_file`

```python
download_file(self, dest: Path | None = None, skip_if_unchanged: bool = False) -> bytes | int
```

Download the file content from this FileObject node.

This method is only available for nodes that inherit from CoreFileObject.
The node must have been saved (have an id) before calling this method.

**Args:**

- `dest`: Optional destination path. If provided, the file will be streamed
  directly to this path (memory-efficient for large files) and the
  number of bytes written will be returned. If not provided, the
  file content will be returned as bytes.
- `skip_if_unchanged`: When ``True``, compute the SHA-1 of the file at
  ``dest`` (which must be provided) and compare against the
  node's ``checksum`` attribute. If they match, return ``0``
  without hitting the network. The ``checksum`` is the value
  loaded when this node was fetched — a later server-side
  change to the file will not be detected unless the caller
  re-fetches the node first.

**Returns:**

- If ``dest`` is None: The file content as bytes.
- If ``dest`` is provided: The number of bytes written to the file.
- If ``skip_if_unchanged=True`` and the local file matches the server checksum: ``0``.

**Raises:**

- `FeatureNotSupportedError`: If this node doesn't inherit from CoreFileObject.
- `ValueError`: If the node hasn't been saved yet, file not found, or
  ``skip_if_unchanged=True`` was passed without a ``dest``.
- `AuthenticationError`: If authentication fails.

**Examples:**

```python
>>> # Download to memory
>>> content = await contract.download_file()
>>> # Stream to file (memory-efficient for large files)
>>> bytes_written = await contract.download_file(dest=Path("/tmp/contract.pdf"))
>>> # Skip download if local file already matches server checksum
>>> bytes_written = await contract.download_file(
...     dest=Path("/tmp/contract.pdf"), skip_if_unchanged=True
... )
```

</details>
#### `matches_local_checksum`

```python
matches_local_checksum(self, source: bytes | Path | BinaryIO) -> bool
```

Return True if ``source``'s SHA-1 matches this node's server checksum.

Only available for nodes inheriting from ``CoreFileObject``. Callers
that want to branch on the comparison without invoking a transfer
should use this primitive instead of reading ``node.checksum.value``
and hashing ``source`` themselves, so the hashing convention stays
centralised in the SDK.

The comparison is against the ``checksum`` attribute as loaded
when this node was retrieved from the server. If the server's
file has been replaced since the node was fetched, this method
will not see that change — re-fetch the node to refresh the
checksum before comparing.

**Args:**

- `source`: Local content to hash and compare. Accepts the same
shapes as \:func\:`infrahub_sdk.file_handler.sha1_of_source`.

**Returns:**

- True if the local digest equals the server's stored checksum.

**Raises:**

- `FeatureNotSupportedError`: Node is not a ``CoreFileObject``.
- `ValueError`: Node has no server-side checksum yet (unsaved or
file never attached).

#### `upload_if_changed`

```python
upload_if_changed(self, source: bytes | Path | BinaryIO, name: str | None = None) -> UploadResult
```

Upload ``source`` only if its SHA-1 differs from the server checksum.

Composes :meth:`matches_local_checksum` with :meth:`upload_from_path`
(or :meth:`upload_from_bytes`) and :meth:`save`. For unsaved nodes or
nodes that have no prior server-side file, the upload is always
performed — there is nothing to compare against.

Idempotency is content-only: when the local SHA-1 matches the server
checksum the upload is skipped even if ``name`` differs from the
server-side filename. Use a regular :meth:`upload_from_path` /
:meth:`save` round-trip if you need to rename without changing
content.

**Args:**

- `source`: Content to upload. ``bytes`` and ``BinaryIO`` sources
must supply ``name``; for a ``Path`` the filename is derived
from ``source.name`` when ``name`` is omitted.
- `name`: Filename to use on the server. Required for ``bytes`` /
``BinaryIO`` sources.

**Returns:**

- class:`UploadResult` with ``was_uploaded=False`` (skipped) or
- ``was_uploaded=True`` (transfer occurred), and the resulting server
- checksum (``None`` only when no server checksum was available
- after the operation).

**Raises:**

- `FeatureNotSupportedError`: Node is not a ``CoreFileObject``.
- `ValueError`: ``source`` is ``bytes`` or ``BinaryIO`` and no
``name`` was supplied.

#### `delete`

```python
delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None
```

Delete this node on the backend.

**Args:**

- `timeout`: Overrides the default timeout used when querying the
GraphQL API. Specified in seconds.
- `request_context`: Request-level context passed through
to the mutation. When omitted, the client's request context is used.

#### `save`

```python
save(self, allow_upsert: bool = False, update_group_context: bool | None = None, timeout: int | None = None, request_context: RequestContext | None = None) -> None
```

Persist this node to the backend, creating or updating it as appropriate.

New nodes are created (or upserted when ``allow_upsert`` is set), and existing
nodes are updated with only the modified fields. After a successful save, the
node is added to the client store and, when applicable, to the active group
context for tracking.

**Args:**

- `allow_upsert`: When ``True``, an existing node is upserted
instead of failing with a duplicate. Defaults to ``False``.
- `update_group_context`: Whether to update the group context
with this node. When ``None`` and the client is in tracking mode, defaults
to ``True``.
- `timeout`: Overrides the default timeout used when querying the
GraphQL API. Specified in seconds.
- `request_context`: Request-level context passed through
to the mutation. When omitted, the client's request context is used.

#### `generate_query_data`

```python
generate_query_data(self, filters: dict[str, Any] | None = None, offset: int | None = None, limit: int | None = None, include: list[str] | None = None, exclude: list[str] | None = None, fragment: bool = False, prefetch_relationships: bool = False, partial_match: bool = False, property: bool = False, order: Order | None = None, include_metadata: bool = False) -> dict[str, Any | dict]
```

Generate the full GraphQL query payload for this node kind.

The returned dict combines :meth:`generate_query_data_init` with
:meth:`generate_query_data_node`. When the node is a generic and ``fragment`` is
``True``, ``...on Kind`` fragments are added for every implementing kind so the
relevant attributes are returned alongside the generic fields.

**Args:**

- `filters`: Filters to apply to the query.
- `offset`: Pagination offset.
- `limit`: Pagination limit.
- `include`: Attributes or relationships to include.
- `exclude`: Attributes or relationships to exclude.
- `fragment`: When ``True`` and the schema is a generic, emit
``...on Kind`` fragments for each implementing kind. Defaults to ``False``.
- `prefetch_relationships`: When ``True``, pre-fetch related node
data instead of returning only their identifiers. Defaults to ``False``.
- `partial_match`: When ``True``, allow partial matches on filter
criteria. Defaults to ``False``.
- `property`: When ``True``, include attribute and relationship
properties (``source``, ``owner``, ``is_protected``, ...). Defaults to ``False``.
- `order`: Ordering options to apply to the query.
- `include_metadata`: When ``True``, include ``node_metadata`` and
``relationship_metadata`` in the result. Defaults to ``False``.

**Returns:**

- dict\[str, Any | dict]: A query payload keyed by the node kind, ready to be
- rendered as GraphQL.

#### `generate_query_data_node`

```python
generate_query_data_node(self, include: list[str] | None = None, exclude: list[str] | None = None, inherited: bool = True, insert_alias: bool = False, prefetch_relationships: bool = False, property: bool = False, include_metadata: bool = False) -> dict[str, Any | dict]
```

Generate the node part of a GraphQL Query with attributes and nodes.

**Args:**

- `include`: List of attributes or relationships to include. Defaults to None.
- `exclude`: List of attributes or relationships to exclude. Defaults to None.
- `inherited`: Indicated of the attributes and the relationships inherited from generics should be included as well.
                        Defaults to True.
- `insert_alias`: If True, inserts aliases in the query for each attribute or relationship.
- `prefetch_relationships`: If True, pre-fetches relationship data as part of the query.
- `include_metadata`: If True, includes node_metadata and relationship_metadata in the query.

**Returns:**

- dict\[str, Union\[Any, Dict]]: GraphQL query in dictionary format

#### `add_relationships`

```python
add_relationships(self, relation_to_update: str, related_nodes: list[str]) -> None
```

Add peers to a cardinality-many relationship through a dedicated mutation.

Unlike :meth:`save`, this method targets a single relationship and only adds
peers, leaving every other field untouched.

**Args:**

- `relation_to_update`: The name of the relationship to update.
- `related_nodes`: The IDs of the peers to add.

#### `remove_relationships`

```python
remove_relationships(self, relation_to_update: str, related_nodes: list[str]) -> None
```

Remove peers from a cardinality-many relationship through a dedicated mutation.

Unlike :meth:`save`, this method targets a single relationship and only removes
the listed peers, leaving every other field untouched.

**Args:**

- `relation_to_update`: The name of the relationship to update.
- `related_nodes`: The IDs of the peers to remove.

#### `create`

```python
create(self, allow_upsert: bool = False, timeout: int | None = None, request_context: RequestContext | None = None) -> None
```

Create this node on the backend.

For nodes inheriting from ``CoreFileObject``, the file content set with
:meth:`upload_from_path` or :meth:`upload_from_bytes` is uploaded as part of the
mutation and cleared from the node afterward.

Prefer :meth:`save` over calling ``create()`` directly so existing-vs-new logic
is handled for you.

**Args:**

- `allow_upsert`: When ``True``, the operation upserts instead of
erroring on a duplicate. Defaults to ``False``.
- `timeout`: Overrides the default timeout used when querying the
GraphQL API. Specified in seconds.
- `request_context`: Request-level context passed through
to the mutation. When omitted, the client's request context is used.

**Raises:**

- `ValueError`: If this is a file-object node and no file content has been set.

#### `update`

```python
update(self, do_full_update: bool = False, timeout: int | None = None, request_context: RequestContext | None = None) -> None
```

Update this node on the backend.

By default only the modified attributes and relationships are sent so the server
can compute a minimal diff. Setting ``do_full_update`` re-sends every field even
when unchanged, which is useful when forcing relationship reconciliation.

Prefer :meth:`save` over calling ``update()`` directly so existing-vs-new logic
is handled for you.

**Args:**

- `do_full_update`: When ``True``, send every field even when
unmodified. Defaults to ``False``.
- `timeout`: Overrides the default timeout used when querying the
GraphQL API. Specified in seconds.
- `request_context`: Request-level context passed through
to the mutation. When omitted, the client's request context is used.

#### `get_pool_allocated_resources`

```python
get_pool_allocated_resources(self, resource: InfrahubNode) -> list[InfrahubNode]
```

Fetch all nodes that were allocated for the pool and a given resource.

**Args:**

- `resource`: The resource from which the nodes were allocated.

**Returns:**

- list\[InfrahubNode]: The allocated nodes.

**Raises:**

- `ValueError`: If the node is not a resource pool.

#### `get_pool_resources_utilization`

```python
get_pool_resources_utilization(self) -> list[dict[str, Any]]
```

Fetch the utilization of each resource for the pool.

**Returns:**

- list\[dict\[str, Any]]: A list containing the allocation numbers for each resource of the pool.

**Raises:**

- `ValueError`: If the node is not a resource pool.

#### `get_flat_value`

```python
get_flat_value(self, key: str, separator: str = '__') -> Any
```

Resolve a value addressed by a flat key over this node and its related nodes.

Walks attributes on this node, descending through cardinality-one relationships
(which are fetched on demand) until the final component is reached. Each
relationship hop incurs a backend call, so this is intended for ad-hoc lookups
rather than bulk traversal.

**Args:**

- `key`: The flat key to resolve (for example ``"name__value"`` or
``"site__name__value"``).
- `separator`: Component separator in ``key``. Defaults to ``"__"``.

**Returns:**

- The resolved value.

**Raises:**

- `ValueError`: If a component does not match an attribute or relationship, or if
a relationship hop targets a non cardinality-one relationship.

**Examples:**

name__value
module.object.value

#### `extract`

```python
extract(self, params: dict[str, str]) -> dict[str, Any]
```

Extract several values addressed by flat keys into a labeled dict.

Each value in ``params`` is resolved with :meth:`get_flat_value`, and the
corresponding key is preserved as the output label.

**Args:**

- `params`: A mapping of output label to flat key to resolve.

**Returns:**

- dict\[str, Any]: The resolved values keyed by their output label.

### `InfrahubNodeSync`

Synchronous Infrahub node bound to an :class:`InfrahubClientSync`.

Synchronous counterpart of :class:`InfrahubNode`. Provides full CRUD against the
backend (:meth:`save`, :meth:`create`, :meth:`update`, :meth:`delete`) along with
relationship traversal (:meth:`get_flat_value`, :meth:`extract`), feature-gated
artifact and resource-pool helpers (:meth:`artifact_generate`,
:meth:`get_pool_allocated_resources`), and file upload or download for nodes
inheriting from ``CoreFileObject``.

Attributes and relationships defined on the schema are exposed as instance
attributes via attribute-style access (``node.name.value``, ``node.parent``).

**Methods:**

#### `from_graphql`

```python
from_graphql(cls, client: InfrahubClientSync, branch: str, data: dict, schema: MainSchemaTypesAPI | None = None, timeout: int | None = None) -> Self
```

Build an :class:`InfrahubNodeSync` from a raw GraphQL response.

When no ``schema`` is provided, the node kind is read from ``__typename`` in the
payload and the schema is fetched from the client.

**Args:**

- `client`: The client used to interact with the backend.
- `branch`: The branch the node belongs to.
- `data`: The GraphQL payload describing the node.
- `schema`: Pre-fetched schema for the node kind.
Skips the schema lookup when provided.
- `timeout`: Overrides the default timeout used when fetching the
schema. Specified in seconds.

**Returns:**

- The hydrated node instance.

**Raises:**

- `ValueError`: If ``__typename`` is missing from ``data`` and no ``schema`` was provided.

#### `generate`

```python
generate(self, nodes: list[str] | None = None) -> None
```

Trigger artifact generation for this artifact definition.

Only available on nodes whose kind is ``CoreArtifactDefinition``.

**Args:**

- `nodes`: The IDs of target nodes to generate artifacts
for. When omitted, generation runs for all targets matched by the
definition.

**Raises:**

- `FeatureNotSupportedError`: If this node is not a ``CoreArtifactDefinition``.

#### `artifact_generate`

```python
artifact_generate(self, name: str) -> None
```

Regenerate a named artifact targeting this node.

Looks up the ``CoreArtifact`` named ``name`` for this node, then calls
:meth:`generate` on the related definition with this artifact's ID.

**Args:**

- `name`: The name of the artifact to regenerate.

**Raises:**

- `FeatureNotSupportedError`: If this node does not inherit from ``CoreArtifactTarget``.

#### `artifact_fetch`

```python
artifact_fetch(self, name: str) -> str | dict[str, Any]
```

Fetch the stored content of a named artifact for this node.

**Args:**

- `name`: The name of the artifact to fetch.

**Returns:**

- str | dict\[str, Any]: The artifact content. Returns a parsed object for
- JSON-typed artifacts and a string for text-typed artifacts.

**Raises:**

- `FeatureNotSupportedError`: If this node does not inherit from ``CoreArtifactTarget``.

#### `download_file`

```python
download_file(self, dest: None = None, skip_if_unchanged: bool = ...) -> bytes
```

<details>
<summary>Show 2 other overloads</summary>

#### `download_file`

```python
download_file(self, dest: Path, skip_if_unchanged: bool = ...) -> int
```

#### `download_file`

```python
download_file(self, dest: Path | None = None, skip_if_unchanged: bool = False) -> bytes | int
```

Download the file content from this FileObject node.

This method is only available for nodes that inherit from CoreFileObject.
The node must have been saved (have an id) before calling this method.

**Args:**

- `dest`: Optional destination path. If provided, the file will be streamed
  directly to this path (memory-efficient for large files) and the
  number of bytes written will be returned. If not provided, the
  file content will be returned as bytes.
- `skip_if_unchanged`: When ``True``, compute the SHA-1 of the file at
  ``dest`` (which must be provided) and compare against the
  node's ``checksum`` attribute. If they match, return ``0``
  without hitting the network. The ``checksum`` is the value
  loaded when this node was fetched — a later server-side
  change to the file will not be detected unless the caller
  re-fetches the node first.

**Returns:**

- If ``dest`` is None: The file content as bytes.
- If ``dest`` is provided: The number of bytes written to the file.
- If ``skip_if_unchanged=True`` and the local file matches the server checksum: ``0``.

**Raises:**

- `FeatureNotSupportedError`: If this node doesn't inherit from CoreFileObject.
- `ValueError`: If the node hasn't been saved yet, file not found, or
  ``skip_if_unchanged=True`` was passed without a ``dest``.
- `AuthenticationError`: If authentication fails.

**Examples:**

```python
>>> # Download to memory
>>> content = contract.download_file()
>>> # Stream to file (memory-efficient for large files)
>>> bytes_written = contract.download_file(dest=Path("/tmp/contract.pdf"))
>>> # Skip download if local file already matches server checksum
>>> bytes_written = contract.download_file(
...     dest=Path("/tmp/contract.pdf"), skip_if_unchanged=True
... )
```

</details>
#### `matches_local_checksum`

```python
matches_local_checksum(self, source: bytes | Path | BinaryIO) -> bool
```

Return True if ``source``'s SHA-1 matches this node's server checksum.

Only available for nodes inheriting from ``CoreFileObject``. Callers
that want to branch on the comparison without invoking a transfer
should use this primitive instead of reading ``node.checksum.value``
and hashing ``source`` themselves, so the hashing convention stays
centralised in the SDK.

The comparison is against the ``checksum`` attribute as loaded
when this node was retrieved from the server. If the server's
file has been replaced since the node was fetched, this method
will not see that change — re-fetch the node to refresh the
checksum before comparing.

**Args:**

- `source`: Local content to hash and compare. Accepts the same
shapes as \:func\:`infrahub_sdk.file_handler.sha1_of_source`.

**Returns:**

- True if the local digest equals the server's stored checksum.

**Raises:**

- `FeatureNotSupportedError`: Node is not a ``CoreFileObject``.
- `ValueError`: Node has no server-side checksum yet (unsaved or
file never attached).

#### `upload_if_changed`

```python
upload_if_changed(self, source: bytes | Path | BinaryIO, name: str | None = None) -> UploadResult
```

Upload ``source`` only if its SHA-1 differs from the server checksum.

Composes :meth:`matches_local_checksum` with :meth:`upload_from_path`
(or :meth:`upload_from_bytes`) and :meth:`save`. For unsaved nodes or
nodes that have no prior server-side file, the upload is always
performed — there is nothing to compare against.

Idempotency is content-only: when the local SHA-1 matches the server
checksum the upload is skipped even if ``name`` differs from the
server-side filename. Use a regular :meth:`upload_from_path` /
:meth:`save` round-trip if you need to rename without changing
content.

**Args:**

- `source`: Content to upload. ``bytes`` and ``BinaryIO`` sources
must supply ``name``; for a ``Path`` the filename is derived
from ``source.name`` when ``name`` is omitted.
- `name`: Filename to use on the server. Required for ``bytes`` /
``BinaryIO`` sources.

**Returns:**

- class:`UploadResult` with ``was_uploaded=False`` (skipped) or
- ``was_uploaded=True`` (transfer occurred), and the resulting server
- checksum (``None`` only when no server checksum was available
- after the operation).

**Raises:**

- `FeatureNotSupportedError`: Node is not a ``CoreFileObject``.
- `ValueError`: ``source`` is ``bytes`` or ``BinaryIO`` and no
``name`` was supplied.

#### `delete`

```python
delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None
```

Delete this node on the backend.

**Args:**

- `timeout`: Overrides the default timeout used when querying the
GraphQL API. Specified in seconds.
- `request_context`: Request-level context passed through
to the mutation. When omitted, the client's request context is used.

#### `save`

```python
save(self, allow_upsert: bool = False, update_group_context: bool | None = None, timeout: int | None = None, request_context: RequestContext | None = None) -> None
```

Persist this node to the backend, creating or updating it as appropriate.

New nodes are created (or upserted when ``allow_upsert`` is set), and existing
nodes are updated with only the modified fields. After a successful save, the
node is added to the client store and, when applicable, to the active group
context for tracking.

**Args:**

- `allow_upsert`: When ``True``, an existing node is upserted
instead of failing with a duplicate. Defaults to ``False``.
- `update_group_context`: Whether to update the group context
with this node. When ``None`` and the client is in tracking mode, defaults
to ``True``.
- `timeout`: Overrides the default timeout used when querying the
GraphQL API. Specified in seconds.
- `request_context`: Request-level context passed through
to the mutation. When omitted, the client's request context is used.

#### `generate_query_data`

```python
generate_query_data(self, filters: dict[str, Any] | None = None, offset: int | None = None, limit: int | None = None, include: list[str] | None = None, exclude: list[str] | None = None, fragment: bool = False, prefetch_relationships: bool = False, partial_match: bool = False, property: bool = False, order: Order | None = None, include_metadata: bool = False) -> dict[str, Any | dict]
```

Generate the full GraphQL query payload for this node kind.

The returned dict combines :meth:`generate_query_data_init` with
:meth:`generate_query_data_node`. When the node is a generic and ``fragment`` is
``True``, ``...on Kind`` fragments are added for every implementing kind so the
relevant attributes are returned alongside the generic fields.

**Args:**

- `filters`: Filters to apply to the query.
- `offset`: Pagination offset.
- `limit`: Pagination limit.
- `include`: Attributes or relationships to include.
- `exclude`: Attributes or relationships to exclude.
- `fragment`: When ``True`` and the schema is a generic, emit
``...on Kind`` fragments for each implementing kind. Defaults to ``False``.
- `prefetch_relationships`: When ``True``, pre-fetch related node
data instead of returning only their identifiers. Defaults to ``False``.
- `partial_match`: When ``True``, allow partial matches on filter
criteria. Defaults to ``False``.
- `property`: When ``True``, include attribute and relationship
properties (``source``, ``owner``, ``is_protected``, ...). Defaults to ``False``.
- `order`: Ordering options to apply to the query.
- `include_metadata`: When ``True``, include ``node_metadata`` and
``relationship_metadata`` in the result. Defaults to ``False``.

**Returns:**

- dict\[str, Any | dict]: A query payload keyed by the node kind, ready to be
- rendered as GraphQL.

#### `generate_query_data_node`

```python
generate_query_data_node(self, include: list[str] | None = None, exclude: list[str] | None = None, inherited: bool = True, insert_alias: bool = False, prefetch_relationships: bool = False, property: bool = False, include_metadata: bool = False) -> dict[str, Any | dict]
```

Generate the node part of a GraphQL Query with attributes and nodes.

**Args:**

- `include`: List of attributes or relationships to include. Defaults to None.
- `exclude`: List of attributes or relationships to exclude. Defaults to None.
- `inherited`: Indicated of the attributes and the relationships inherited from generics should be included as well.
                        Defaults to True.
- `insert_alias`: If True, inserts aliases in the query for each attribute or relationship.
- `prefetch_relationships`: If True, pre-fetches relationship data as part of the query.
- `include_metadata`: If True, includes node_metadata and relationship_metadata in the query.

**Returns:**

- dict\[str, Union\[Any, Dict]]: GraphQL query in dictionary format

#### `add_relationships`

```python
add_relationships(self, relation_to_update: str, related_nodes: list[str]) -> None
```

Add peers to a cardinality-many relationship through a dedicated mutation.

Unlike :meth:`save`, this method targets a single relationship and only adds
peers, leaving every other field untouched.

**Args:**

- `relation_to_update`: The name of the relationship to update.
- `related_nodes`: The IDs of the peers to add.

#### `remove_relationships`

```python
remove_relationships(self, relation_to_update: str, related_nodes: list[str]) -> None
```

Remove peers from a cardinality-many relationship through a dedicated mutation.

Unlike :meth:`save`, this method targets a single relationship and only removes
the listed peers, leaving every other field untouched.

**Args:**

- `relation_to_update`: The name of the relationship to update.
- `related_nodes`: The IDs of the peers to remove.

#### `create`

```python
create(self, allow_upsert: bool = False, timeout: int | None = None, request_context: RequestContext | None = None) -> None
```

Create this node on the backend.

For nodes inheriting from ``CoreFileObject``, the file content set with
:meth:`upload_from_path` or :meth:`upload_from_bytes` is uploaded as part of the
mutation and cleared from the node afterward.

Prefer :meth:`save` over calling ``create()`` directly so existing-vs-new logic
is handled for you.

**Args:**

- `allow_upsert`: When ``True``, the operation upserts instead of
erroring on a duplicate. Defaults to ``False``.
- `timeout`: Overrides the default timeout used when querying the
GraphQL API. Specified in seconds.
- `request_context`: Request-level context passed through
to the mutation. When omitted, the client's request context is used.

**Raises:**

- `ValueError`: If this is a file-object node and no file content has been set.

#### `update`

```python
update(self, do_full_update: bool = False, timeout: int | None = None, request_context: RequestContext | None = None) -> None
```

Update this node on the backend.

By default only the modified attributes and relationships are sent so the server
can compute a minimal diff. Setting ``do_full_update`` re-sends every field even
when unchanged, which is useful when forcing relationship reconciliation.

Prefer :meth:`save` over calling ``update()`` directly so existing-vs-new logic
is handled for you.

**Args:**

- `do_full_update`: When ``True``, send every field even when
unmodified. Defaults to ``False``.
- `timeout`: Overrides the default timeout used when querying the
GraphQL API. Specified in seconds.
- `request_context`: Request-level context passed through
to the mutation. When omitted, the client's request context is used.

#### `get_pool_allocated_resources`

```python
get_pool_allocated_resources(self, resource: InfrahubNodeSync) -> list[InfrahubNodeSync]
```

Fetch all nodes that were allocated for the pool and a given resource.

**Args:**

- `resource`: The resource from which the nodes were allocated.

**Returns:**

- list\[InfrahubNodeSync]: The allocated nodes.

**Raises:**

- `ValueError`: If the node is not a resource pool.

#### `get_pool_resources_utilization`

```python
get_pool_resources_utilization(self) -> list[dict[str, Any]]
```

Fetch the utilization of each resource for the pool.

**Returns:**

- list\[dict\[str, Any]]: A list containing the allocation numbers for each resource of the pool.

**Raises:**

- `ValueError`: If the node is not a resource pool.

#### `get_flat_value`

```python
get_flat_value(self, key: str, separator: str = '__') -> Any
```

Resolve a value addressed by a flat key over this node and its related nodes.

Walks attributes on this node, descending through cardinality-one relationships
(which are fetched on demand) until the final component is reached. Each
relationship hop incurs a backend call, so this is intended for ad-hoc lookups
rather than bulk traversal.

**Args:**

- `key`: The flat key to resolve (for example ``"name__value"`` or
``"site__name__value"``).
- `separator`: Component separator in ``key``. Defaults to ``"__"``.

**Returns:**

- The resolved value.

**Raises:**

- `ValueError`: If a component does not match an attribute or relationship, or if
a relationship hop targets a non cardinality-one relationship.

**Examples:**

name__value
module.object.value

#### `extract`

```python
extract(self, params: dict[str, str]) -> dict[str, Any]
```

Extract several values addressed by flat keys into a labeled dict.

Each value in ``params`` is resolved with :meth:`get_flat_value`, and the
corresponding key is preserved as the output label.

**Args:**

- `params`: A mapping of output label to flat key to resolve.

**Returns:**

- dict\[str, Any]: The resolved values keyed by their output label.

### `UploadResult`

Outcome of an idempotent upload attempt.

Returned by :meth:`InfrahubNode.upload_if_changed` and its sync twin.
``was_uploaded`` tells the caller whether a network transfer actually
happened; ``checksum`` carries the SHA-1 of the content held on the
server after the operation — on skip paths that is the server's
pre-existing value, on upload paths it is the locally-computed SHA-1
used as a proxy (which matches what a standard CoreFileObject server
stores, since the server computes SHA-1 of received bytes). ``None``
only when no server checksum was available (either the node was
unsaved and nothing was transferred, or the save returned no checksum
value).

The comparison used by ``upload_if_changed`` reads the node's
``checksum`` attribute, which was populated when the node was
fetched via ``client.get(...)``. A server-side change to the file
between the fetch and the call will not be detected unless the
caller re-fetches the node first.

### `InfrahubNodeBase`

Base class for :class:`InfrahubNode` and :class:`InfrahubNodeSync`.

Owns the schema-driven state shared between the async and sync clients: attributes,
relationships, identity (``id``, ``hfid``), node metadata, and the helpers that turn
the in-memory state into GraphQL query and mutation payloads. This class is not
meant to be instantiated directly; use :class:`InfrahubNode` or
:class:`InfrahubNodeSync` instead.

**Attributes:**

- `id`: The unique identifier of the node, when known.
- `display_label`: Human-readable label of the node.
- `typename`: The GraphQL ``__typename`` of the node.

**Methods:**

#### `get_branch`

```python
get_branch(self) -> str
```

Return the branch this node is bound to.

**Returns:**

- The name of the branch.

#### `get_path_value`

```python
get_path_value(self, path: str) -> Any
```

Resolve a value addressed by a dunder-separated path on this node.

The path can target an attribute (``name__value``, ``name__source``), a
cardinality-one related node (``parent``), an attribute of that related node
(``parent__name__value``), or a property of one of its attributes
(``parent__name__source``).

**Args:**

- `path`: A path with components separated by ``__``.

**Returns:**

- The resolved value, or ``None`` when any path component cannot be
- resolved (for example, an unfetched related node not present in the store).

#### `get_human_friendly_id`

```python
get_human_friendly_id(self) -> list[str] | None
```

Compute the human-friendly ID for this node from its schema.

The HFID is composed of the values addressed by the schema's
``human_friendly_id`` paths. When any component cannot be resolved, the HFID is
considered invalid and ``None`` is returned.

**Returns:**

- list[str] | None: The HFID as a list of stringified components, or ``None``
- when the schema does not define an HFID or a component is missing.

#### `get_human_friendly_id_as_string`

```python
get_human_friendly_id_as_string(self, include_kind: bool = False) -> str | None
```

Return the human-friendly ID joined into a single string.

**Args:**

- `include_kind`: When ``True``, the node kind is prepended as
the first component of the resulting string. Defaults to ``False``.

**Returns:**

- str | None: The HFID joined with the HFID separator, or ``None`` when no
- HFID is available.

#### `hfid`

```python
hfid(self) -> list[str] | None
```

Return the human-friendly ID of this node as a list of components.

**Returns:**

- list\[str] | None: The HFID components, or ``None`` when unavailable.

#### `hfid_str`

```python
hfid_str(self) -> str | None
```

Return the human-friendly ID of this node as a string, including the kind prefix.

**Returns:**

- str | None: The HFID as ``Kind__part1__part2``, or ``None`` when unavailable.

#### `get_node_metadata`

```python
get_node_metadata(self) -> NodeMetadata | None
```

Return the node metadata (``created_at``, ``created_by``, ``updated_at``, ``updated_by``).

The metadata is populated only when the parent query was executed with
``include_metadata=True``.

**Returns:**

- NodeMetadata | None: The node metadata if fetched, otherwise ``None``.

#### `get_kind`

```python
get_kind(self) -> str
```

Return the schema kind of this node.

**Returns:**

- The schema kind (for example ``"CoreAccount"``).

#### `get_all_kinds`

```python
get_all_kinds(self) -> list[str]
```

Return this node's kind plus all generic kinds it inherits from.

**Returns:**

- list\[str]: The node's own kind followed by the inherited kinds, in the order
- declared on the schema.

#### `is_ip_prefix`

```python
is_ip_prefix(self) -> bool
```

Return whether this node represents an IP prefix.

**Returns:**

- ``True`` when the node kind is ``BuiltinIPPrefix`` or inherits from it.

#### `is_ip_address`

```python
is_ip_address(self) -> bool
```

Return whether this node represents an IP address.

**Returns:**

- ``True`` when the node kind is ``BuiltinIPAddress`` or inherits from it.

#### `is_resource_pool`

```python
is_resource_pool(self) -> bool
```

Return whether this node is a resource pool.

**Returns:**

- ``True`` when the node inherits from ``CoreResourcePool``.

#### `is_file_object`

```python
is_file_object(self) -> bool
```

Return whether this node inherits from ``CoreFileObject`` and supports file uploads.

**Returns:**

- ``True`` when file upload/download operations are supported on this node.

#### `upload_from_path`

```python
upload_from_path(self, path: Path) -> None
```

Set a file from disk to be uploaded when saving this FileObject node.

The file will be streamed during upload, avoiding loading the entire file into memory.

**Args:**

- `path`: Path to the file on disk.

**Raises:**

- `FeatureNotSupportedError`: If this node doesn't inherit from CoreFileObject.

#### `upload_from_bytes`

```python
upload_from_bytes(self, content: bytes | BinaryIO, name: str) -> None
```

Set content to be uploaded when saving this FileObject node.

The content can be provided as bytes or a file-like object.
Using BinaryIO is recommended for large content to stream during upload.

**Args:**

- `content`: The file content as bytes or a file-like object.
- `name`: The filename to use for the uploaded file.

**Raises:**

- `FeatureNotSupportedError`: If this node doesn't inherit from CoreFileObject.

**Examples:**

```python
>>> # Using bytes (for small files)
>>> node.upload_from_bytes(content=b"file content", name="example.txt")
>>> # Using file-like object (for large files)
>>> with open("/path/to/file.bin", "rb") as f:
...     node.upload_from_bytes(content=f, name="file.bin")
```

#### `clear_file`

```python
clear_file(self) -> None
```

Clear any pending file content.

#### `get_raw_graphql_data`

```python
get_raw_graphql_data(self) -> dict | None
```

Return the raw GraphQL payload used to build this node.

**Returns:**

- dict | None: The original GraphQL data, or ``None`` when the node was
- constructed without payload (for example, a brand-new node).

#### `generate_query_data_init`

```python
generate_query_data_init(self, filters: dict[str, Any] | None = None, offset: int | None = None, limit: int | None = None, include: list[str] | None = None, exclude: list[str] | None = None, partial_match: bool = False, order: Order | None = None, include_metadata: bool = False) -> dict[str, Any | dict]
```

Build the top-level ``count``/``edges`` skeleton of a GraphQL query for this kind.

The returned dict is the outer structure consumed by
:meth:`generate_query_data`; it carries the ``@filters`` block and the empty
``edges.node`` placeholder that will later be filled by the caller.

**Args:**

- `filters`: Filters to apply to the query.
- `offset`: Pagination offset.
- `limit`: Pagination limit.
- `include`: Attributes or relationships to include.
- `exclude`: Attributes or relationships to exclude.
- `partial_match`: When ``True``, allow partial matches on filter
criteria. Defaults to ``False``.
- `order`: Ordering options to apply to the query.
- `include_metadata`: When ``True``, include ``node_metadata`` in
the result. Defaults to ``False``.

**Returns:**

- dict[str, Any | dict]: The query skeleton ready to be combined with node-level
- attributes and relationships.

**Raises:**

- `ValueError`: If the same name appears in both ``include`` and ``exclude``.
