Utilities

Two building blocks the SDK uses internally and exposes for the same jobs in your own code: computing a surgical delta between two objects, and unwrapping an APIC response envelope.

Diffing

ManagedObject diff utility for surgical APIC updates.

mo_diff() compares a desired state object against a current state object (typically retrieved from the APIC via from_apic()) and returns a new instance carrying only the changed fields.

The returned object’s model_fields_set contains exclusively the naming props plus the fields that actually differ, so calling to_apic() on it produces a surgical PATCH payload — the APIC leaves untouched fields at their current values.

Children are diffed recursively by default (see recurse_children): matched by (_aci_class, naming prop values) identity. Children present in desired but absent in current are included as-is (additions). Children present only in current are ignored — mo_diff does not produce DELETE ops; handle removals explicitly via the facade.

Example:

from niwaki.models._generated.fv.fvBD import fvBD
from niwaki.utils.diff import mo_diff

desired = fvBD(name="web", unicast_routing=False, arp_flooding=True)
current = fvBD.model_validate({"name": "web", "unicastRoute": "yes", "arpFlood": "no"})

delta = mo_diff(desired, current)
if delta:
    delta.to_apic()
    # → {"fvBD": {"attributes": {"name": "web",
    #                            "unicastRoute": "false",
    #                            "arpFlood": "true"}}}
niwaki.utils.diff.mo_diff(desired, current, *, recurse_children=True, respect_fields_set=False)[source]

Compute the delta between a desired and current ManagedObject state.

Only declared model fields are compared — APIC-originated read-only extra fields (modTs, uid, …) stored in model_extra are ignored on both sides.

Parameters:
  • desired (T) – Target state — the object as you want it to be.

  • current (T) – Current state — typically from from_apic().

  • recurse_children (bool) – When True (default), children are diffed recursively and any changed or new children are included in the returned delta. Set to False to restrict the diff to scalar fields only (original behaviour).

  • respect_fields_set (bool) – When True, only fields present in desired.model_fields_set are compared — fields the caller never touched are ignored even if the current state differs from the schema default. This is the mode used by the design DSL’s plan push: a design that does not set unicast_routing must not report a change just because the APIC value differs from the model default. Default False (compare all declared fields — original behaviour).

Returns:

A new instance of the same class with model_fields_set limited to naming props + changed fields (+ changed children when recurse_children=True). Returns None when the objects are identical (no diff to apply).

Raises:
  • TypeError – When desired and current are of different classes.

  • ValueError – When a naming prop differs between the two objects (they represent different objects and cannot be diffed).

Return type:

T | None

Example:

desired = fvBD(name="web", unicast_routing=False)
current = fvBD.model_validate({"name": "web", "unicastRoute": "yes"})

delta = mo_diff(desired, current)
# delta.to_apic() → {"fvBD": {"attributes": {"name": "web", "unicastRoute": "false"}}}

mo_diff(desired, desired)  # → None  (no change)

Children example:

from niwaki.models._generated.fv.fvSubnet import fvSubnet

desired_bd = fvBD(name="web")
desired_bd.children = [fvSubnet(ip="10.1.0.1/24", scope="public")]

current_bd = fvBD.model_validate({"name": "web"})
current_bd.children = [fvSubnet(ip="10.1.0.1/24", scope="private")]

delta = mo_diff(desired_bd, current_bd)
# delta has one child: fvSubnet with scope="public"

Response parsing

APIC REST response parsing utilities.

APIC responses always have the envelope:

{
    "totalCount": "42",
    "imdata": [
        {"fvBD": {"attributes": {...}, "children": [...]}},
        ...
    ]
}

parse_imdata() consumes that envelope and returns typed ManagedObjects. HTTP-level error extraction lives in niwaki.transport._errors.

niwaki.utils.response.parse_imdata(data)[source]

Deserialise all objects in the imdata array into typed ManagedObjects.

Uses from_apic() for each item, which dispatches to the correct generated subclass via REGISTRY. Error entries (items keyed "error") are skipped — HTTP status handling raises typed exceptions before this parser runs.

Parameters:

data (dict[str, Any]) – Raw APIC response dict (the top-level JSON object).

Returns:

List of ManagedObject instances (may be empty).

Raises:

niwaki.exceptions.DeserializationError – When any item in imdata cannot be deserialised.

Return type:

list[ManagedObject]

Example:

raw = {"totalCount": "2", "imdata": [
    {"fvBD": {"attributes": {"name": "web"}}},
    {"fvBD": {"attributes": {"name": "db"}}},
]}
bds = parse_imdata(raw)
# → [fvBD(name="web"), fvBD(name="db")]