Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/325.removed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Removed `client.branch.diff_data()` from both the async and sync clients. The method relied on a `GET /api/diff/data` REST endpoint that does not exist in Infrahub, so every call returned a 404. Use `client.get_diff_tree()` instead, which now accepts `include_properties=True` to also return the value-level details of each change (previous/new value per attribute property, peer id/label per relationship element), or `client.get_diff_summary()` for the list of changed nodes; both use the `DiffTree` GraphQL query.
33 changes: 29 additions & 4 deletions docs/docs/python-sdk/guides/branches.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,40 @@ The Python SDK provides multiple methods to manage the branches in an Infrahub i
</TabItem>
</Tabs>

## Generating a diff for a branch
## Getting the diff for a branch

Use `get_diff_tree` to retrieve the full diff of a branch compared to its base branch, including summary counts and the list of changed nodes. It returns `None` if no diff exists for the branch. Set `include_properties=True` to also retrieve the value-level details of each change (previous and new value per property).

<Tabs groupId="async-sync">
<TabItem value="Async" default>

```python
from infrahub_sdk import InfrahubClient
client = InfrahubClient()
diff = await client.get_diff_tree(branch="new-branch", include_properties=True)
```

</TabItem>
<TabItem value="Sync" default>

```python
from infrahub_sdk import InfrahubClientSync
client = InfrahubClientSync()
diff = client.get_diff_tree(branch="new-branch", include_properties=True)
```

</TabItem>
</Tabs>

If you only need the list of changed nodes, `get_diff_summary` returns them without the diff metadata.

<Tabs groupId="async-sync">
<TabItem value="Async" default>

```python
from infrahub_sdk import InfrahubClient
client = await InfrahubClient()
diff = await client.branch.diff_data(branch_name="new-branch")
client = InfrahubClient()
node_diffs = await client.get_diff_summary(branch="new-branch")
```

</TabItem>
Expand All @@ -125,7 +150,7 @@ The Python SDK provides multiple methods to manage the branches in an Infrahub i
```python
from infrahub_sdk import InfrahubClientSync
client = InfrahubClientSync()
diff = client.branch.diff_data(branch_name="new-branch")
node_diffs = client.get_diff_summary(branch="new-branch")
```

</TabItem>
Expand Down
10 changes: 8 additions & 2 deletions docs/docs/python-sdk/sdk_ref/infrahub_sdk/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -403,11 +403,14 @@ get_diff_summary(self, branch: str, name: str | None = None, from_time: datetime
#### `get_diff_tree`

```python
get_diff_tree(self, branch: str, name: str | None = None, from_time: datetime | None = None, to_time: datetime | None = None, timeout: int | None = None, tracker: str | None = None, priority: Priority | None = None) -> DiffTreeData | None
get_diff_tree(self, branch: str, name: str | None = None, from_time: datetime | None = None, to_time: datetime | None = None, timeout: int | None = None, tracker: str | None = None, priority: Priority | None = None, include_properties: bool = False) -> DiffTreeData | None
```

Get complete diff tree with metadata and nodes.

Set ``include_properties`` to True to also retrieve the value-level details
of each change (previous/new values per property).

Returns None if no diff exists.

**Raises:**
Expand Down Expand Up @@ -932,11 +935,14 @@ get_diff_summary(self, branch: str, name: str | None = None, from_time: datetime
#### `get_diff_tree`

```python
get_diff_tree(self, branch: str, name: str | None = None, from_time: datetime | None = None, to_time: datetime | None = None, timeout: int | None = None, tracker: str | None = None, priority: Priority | None = None) -> DiffTreeData | None
get_diff_tree(self, branch: str, name: str | None = None, from_time: datetime | None = None, to_time: datetime | None = None, timeout: int | None = None, tracker: str | None = None, priority: Priority | None = None, include_properties: bool = False) -> DiffTreeData | None
```

Get complete diff tree with metadata and nodes.

Set ``include_properties`` to True to also retrieve the value-level details
of each change (previous/new values per property).

Returns None if no diff exists.

**Raises:**
Expand Down
65 changes: 3 additions & 62 deletions infrahub_sdk/branch.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from __future__ import annotations

from enum import Enum
from typing import TYPE_CHECKING, Any, Literal, overload
from urllib.parse import urlencode
from typing import TYPE_CHECKING, Literal, overload

from pydantic import BaseModel

from .exceptions import BranchNotFoundError
from .graphql import Mutation, Query
from .utils import decode_json

if TYPE_CHECKING:
from .client import InfrahubClient, InfrahubClientSync
Expand Down Expand Up @@ -61,30 +59,7 @@ class BranchData(BaseModel):
QUERY_ONE_BRANCH_DATA = {"Branch": {**BRANCH_DATA, **BRANCH_DATA_FILTER}}


class InfraHubBranchManagerBase:
@classmethod
def generate_diff_data_url(
cls,
client: InfrahubClient | InfrahubClientSync,
branch_name: str,
branch_only: bool = True,
time_from: str | None = None,
time_to: str | None = None,
) -> str:
"""Generate the URL for the diff_data function."""
url = f"{client.address}/api/diff/data"
url_params = {}
url_params["branch"] = branch_name
url_params["branch_only"] = str(branch_only).lower()
if time_from:
url_params["time_from"] = time_from
if time_to:
url_params["time_to"] = time_to

return url + urlencode(url_params)


class InfrahubBranchManager(InfraHubBranchManagerBase):
class InfrahubBranchManager:
def __init__(self, client: InfrahubClient) -> None:
self.client = client

Expand Down Expand Up @@ -206,25 +181,8 @@ async def get(self, branch_name: str) -> BranchData:
raise BranchNotFoundError(identifier=branch_name)
return BranchData(**data["Branch"][0])

async def diff_data(
self,
branch_name: str,
branch_only: bool = True,
time_from: str | None = None,
time_to: str | None = None,
) -> dict[Any, Any]:
url = self.generate_diff_data_url(
client=self.client,
branch_name=branch_name,
branch_only=branch_only,
time_from=time_from,
time_to=time_to,
)
response = await self.client._get(url=url, headers=self.client.headers)
return decode_json(response=response)


class InfrahubBranchManagerSync(InfraHubBranchManagerBase):
class InfrahubBranchManagerSync:
def __init__(self, client: InfrahubClientSync) -> None:
self.client = client

Expand Down Expand Up @@ -300,23 +258,6 @@ def delete(self, branch_name: str) -> bool:
response = self.client.execute_graphql(query=query.render(), tracker="mutation-branch-delete")
return response["BranchDelete"]["ok"]

def diff_data(
self,
branch_name: str,
branch_only: bool = True,
time_from: str | None = None,
time_to: str | None = None,
) -> dict[Any, Any]:
url = self.generate_diff_data_url(
client=self.client,
branch_name=branch_name,
branch_only=branch_only,
time_from=time_from,
time_to=time_to,
)
response = self.client._get(url=url, headers=self.client.headers)
return decode_json(response=response)

def merge(self, branch_name: str) -> bool:
input_data = {
"data": {
Expand Down
12 changes: 10 additions & 2 deletions infrahub_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1841,16 +1841,20 @@ async def get_diff_tree(
timeout: int | None = None,
tracker: str | None = None,
priority: Priority | None = None,
include_properties: bool = False,
) -> DiffTreeData | None:
"""Get complete diff tree with metadata and nodes.

Set ``include_properties`` to True to also retrieve the value-level details
of each change (previous/new values per property).

Returns None if no diff exists.

Raises:
ValueError: If ``from_time`` is later than ``to_time``.

"""
query = get_diff_tree_query()
query = get_diff_tree_query(include_properties=include_properties)
input_data = {"branch_name": branch}
if name:
input_data["name"] = name
Expand Down Expand Up @@ -3433,16 +3437,20 @@ def get_diff_tree(
timeout: int | None = None,
tracker: str | None = None,
priority: Priority | None = None,
include_properties: bool = False,
) -> DiffTreeData | None:
"""Get complete diff tree with metadata and nodes.

Set ``include_properties`` to True to also retrieve the value-level details
of each change (previous/new values per property).

Returns None if no diff exists.

Raises:
ValueError: If ``from_time`` is later than ``to_time``.

"""
query = get_diff_tree_query()
query = get_diff_tree_query(include_properties=include_properties)
input_data = {"branch_name": branch}
if name:
input_data["name"] = name
Expand Down
98 changes: 83 additions & 15 deletions infrahub_sdk/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ class NodeDiff(TypedDict):
elements: list[NodeDiffElement]


class NodeDiffElement(TypedDict):
class NodeDiffPeerFields(TypedDict, total=False):
peer_id: str
peer_label: str | None
properties: list[NodeDiffProperty]


class NodeDiffElement(NodeDiffPeerFields):
name: str
element_type: str
action: str
Expand All @@ -31,11 +37,20 @@ class NodeDiffSummary(TypedDict):
removed: int


class NodeDiffPeer(TypedDict):
class NodeDiffPeer(NodeDiffPeerFields):
action: str
summary: NodeDiffSummary


class NodeDiffProperty(TypedDict):
property_type: str
action: str
previous_value: str | None
new_value: str | None
previous_label: str | None
new_label: str | None


class DiffTreeData(TypedDict):
num_added: int
num_updated: int
Expand Down Expand Up @@ -88,6 +103,43 @@ def get_diff_summary_query() -> str:
"""


def _diff_properties_to_node_diff_properties(property_dicts: list[dict[str, Any]]) -> list[NodeDiffProperty]:
return [
NodeDiffProperty(
property_type=str(property_dict.get("property_type")),
action=str(property_dict.get("status")),
previous_value=property_dict.get("previous_value"),
new_value=property_dict.get("new_value"),
previous_label=property_dict.get("previous_label"),
new_label=property_dict.get("new_label"),
)
for property_dict in property_dicts
]


def _element_to_node_diff_peer_fields(element_dict: dict[str, Any]) -> NodeDiffPeerFields:
fields = NodeDiffPeerFields()
if "peer_id" in element_dict:
fields["peer_id"] = str(element_dict["peer_id"])
fields["peer_label"] = element_dict.get("peer_label")
if element_dict.get("properties"):
fields["properties"] = _diff_properties_to_node_diff_properties(element_dict["properties"])
return fields


def _diff_element_to_node_diff_peer(element_dict: dict[str, Any]) -> NodeDiffPeer:
peer_diff = NodeDiffPeer(
action=str(element_dict.get("status")),
summary={
"added": int(element_dict.get("num_added") or 0),
"removed": int(element_dict.get("num_removed") or 0),
"updated": int(element_dict.get("num_updated") or 0),
},
)
peer_diff.update(_element_to_node_diff_peer_fields(element_dict))
return peer_diff


def diff_tree_node_to_node_diff(node_dict: dict[str, Any], branch_name: str) -> NodeDiff:
element_diffs: list[NodeDiffElement] = []
if "attributes" in node_dict:
Expand All @@ -102,6 +154,8 @@ def diff_tree_node_to_node_diff(node_dict: dict[str, Any], branch_name: str) ->
"updated": int(attr_dict.get("num_updated") or 0),
},
)
if attr_dict.get("properties"):
attr_diff["properties"] = _diff_properties_to_node_diff_properties(attr_dict["properties"])
element_diffs.append(attr_diff)
if "relationships" in node_dict:
for relationship_dict in node_dict["relationships"]:
Expand All @@ -116,19 +170,19 @@ def diff_tree_node_to_node_diff(node_dict: dict[str, Any], branch_name: str) ->
"updated": int(relationship_dict.get("num_updated") or 0),
},
)
element_dicts = relationship_dict.get("elements") or []
if not is_cardinality_one and "elements" in relationship_dict:
peer_diffs = [
NodeDiffPeer(
action=str(element_dict.get("status")),
summary={
"added": int(element_dict.get("num_added") or 0),
"removed": int(element_dict.get("num_removed") or 0),
"updated": int(element_dict.get("num_updated") or 0),
},
)
for element_dict in relationship_dict["elements"]
relationship_diff["peers"] = [
_diff_element_to_node_diff_peer(element_dict) for element_dict in element_dicts
]
elif is_cardinality_one and len(element_dicts) == 1:
relationship_diff.update(_element_to_node_diff_peer_fields(element_dicts[0]))
elif is_cardinality_one and element_dicts:
# a cardinality-one diff normally has a single element; if the server
# ever returns several, keep them all instead of flattening one
relationship_diff["peers"] = [
_diff_element_to_node_diff_peer(element_dict) for element_dict in element_dicts
]
relationship_diff["peers"] = peer_diffs
element_diffs.append(relationship_diff)
return NodeDiff(
branch=branch_name,
Expand All @@ -140,8 +194,8 @@ def diff_tree_node_to_node_diff(node_dict: dict[str, Any], branch_name: str) ->
)


def get_diff_tree_query() -> Query:
node_structure = {
def get_diff_tree_query(include_properties: bool = False) -> Query:
node_structure: dict[str, Any] = {
"uuid": None,
"kind": None,
"status": None,
Expand Down Expand Up @@ -172,6 +226,20 @@ def get_diff_tree_query() -> Query:
},
}

if include_properties:
property_structure = {
"property_type": None,
"status": None,
"previous_value": None,
"new_value": None,
"previous_label": None,
"new_label": None,
}
node_structure["attributes"]["properties"] = property_structure
node_structure["relationships"]["elements"]["peer_id"] = None
node_structure["relationships"]["elements"]["peer_label"] = None
node_structure["relationships"]["elements"]["properties"] = property_structure

return Query(
name="GetDiffTree",
query={
Expand Down
Loading
Loading