The design DSL

Niwaki design DSL — declarative, IDE-friendly ACI provisioning.

Build a detached design tree with operator vocabulary (no APIC class names, no session), then validate and push it in one call. The DSL covers the whole uni subtree — tenants, access policies (infra), fabric policies (fabric), and controller policies — through one uniform vocabulary:

from niwaki import Niwaki
from niwaki.design import tenant

config = (
    tenant("prod")
    .app("prod")
        .epg("frontend").bind(bd="frontend")
    .bd("frontend")
        .set(unicast_routing=True)
        .bind(vrf="prod")
        .subnet("10.0.1.1/24")
    .vrf("prod")
)

with Niwaki("https://apic.example.com", "admin", "secret") as aci:
    config.push(aci, mode="strict")   # one atomic POST, all-or-nothing

Every design is rooted at polUni; design() starts an empty multi-domain design, while tenant() / infra() / fabric() / controller() are shorthands that declare the first domain and return its cursor — sibling domains stay one maker call away:

cfg = design()
cfg.fabric().datetime_policy("prod-ntp")
cfg.infra().vlan_pool("prod", "static").range("vlan-100", "vlan-199")
cfg.tenant("prod").vrf("main")

Core rules (see the Design-first architecture page in the documentation):

  • Structure is literal: every maker maps 1:1 to a real APIC child class; nothing is silently created or hidden.

  • Verbatim is translated: names and parameters use operator vocabulary (entry("http", tcp=80), scope="vrf").

  • References are lazy: bind() / provide() / consume() resolve at push time — forward references allowed, closed-world validated.

  • No I/O during construction: transport is injected at push() only.

Root factories

Each root opens a curated, fully typed surface: the makers, their keyword arguments, the bind() aliases and the verbs available at every position are generated and documented in the DSL reference.

niwaki.design.design()[source]

Create an empty detached design rooted at polUni (uni).

The uniform entry point for multi-domain designs: every other root factory (tenant(), infra(), fabric(), controller()) is sugar for design().<maker>(...). No session and no I/O — the tree is built in memory and pushed later via push().

Returns:

Root UniCursor of a new empty design.

Return type:

UniCursor

Example:

cfg = design()
cfg.fabric().datetime_policy("prod-ntp")
cfg.infra().vlan_pool("prod", "static")
cfg.tenant("prod").vrf("main")
payload = cfg.to_payload()   # one atomic polUni envelope
niwaki.design.tenant(
name,
*,
annotation=None,
description=None,
display_name=None,
owner_key=None,
owner_tag=None,
userdom=None,
)[source]

Create a detached design and declare its fvTenant root.

Shorthand for design().tenant(...) — the returned cursor sits on the declared object; the design root is the enclosing polUni node, so sibling domains stay reachable through the implicit-pop makers.

Returns:

TenantCursor on the new fvTenant node.

Return type:

TenantCursor

niwaki.design.infra(*, annotation=None, display_name=None, owner_key=None, owner_tag=None, userdom=None)[source]

Create a detached design and declare its infraInfra root.

Shorthand for design().infra(...) — the returned cursor sits on the declared object; the design root is the enclosing polUni node, so sibling domains stay reachable through the implicit-pop makers.

Returns:

InfraCursor on the new infraInfra node.

Return type:

InfraCursor

niwaki.design.fabric(
*,
annotation=None,
name=None,
display_name=None,
owner_key=None,
owner_tag=None,
userdom=None,
)[source]

Create a detached design and declare its fabricInst root.

Shorthand for design().fabric(...) — the returned cursor sits on the declared object; the design root is the enclosing polUni node, so sibling domains stay reachable through the implicit-pop makers.

Returns:

FabricCursor on the new fabricInst node.

Return type:

FabricCursor

niwaki.design.controller(
*,
annotation=None,
name=None,
display_name=None,
owner_key=None,
owner_tag=None,
userdom=None,
)[source]

Create a detached design and declare its ctrlrInst root.

Shorthand for design().controller(...) — the returned cursor sits on the declared object; the design root is the enclosing polUni node, so sibling domains stay reachable through the implicit-pop makers.

Returns:

ControllerCursor on the new ctrlrInst node.

Return type:

ControllerCursor

niwaki.design.aaa(
*,
name=None,
annotation=None,
description=None,
display_name=None,
owner_key=None,
owner_tag=None,
password_strength_check=None,
userdom=None,
)[source]

Create a detached design and declare its aaaUserEp root.

Shorthand for design().aaa(...) — the returned cursor sits on the declared object; the design root is the enclosing polUni node, so sibling domains stay reachable through the implicit-pop makers.

Returns:

AaaCursor on the new aaaUserEp node.

Return type:

AaaCursor

References that carry configuration

A bind(), a bind_dn() or a verb usually takes a plain name; wrap the target in ref() when the relationship object itself carries configuration (a domain attachment’s immediacy, a subject filter’s log directive, a node’s management address).

niwaki.design.ref(target, **attrs)[source]

Reference target and configure the relationship itself.

Parameters:
  • target (str) – Name of the referenced object — or, under bind_dn(), its raw DN.

  • **attrs (Any) – Fields to set on the relationship (Rs) object, e.g. encap="vlan-201", directives="log", direction="import".

Returns:

A Ref usable anywhere a plain name is – bind(), bind_dn() and the contract verbs.

Return type:

Ref

Example

>>> from niwaki.design import ref, tenant
>>> cfg = tenant("prod")
>>> cfg.filter("http").entry("tcp-80", tcp=80)
<Cursor ...>
>>> subject = cfg.contract("web").subject("http")
>>> _ = subject.bind(filter=ref("http", directives="log"))
class niwaki.design.Ref(target, attrs)[source]

Bases: object

A reference that carries configuration of its own.

Most relationships in the MIT are pure edges — the Rs object stores the target and nothing else, so naming the target is the whole reference:

epg.bind(bd="web")

Some carry configuration: an EPG-to-domain attachment holds the static encap and the resolution immediacy, a filter attached to a subject holds its directives, a route-control profile attachment holds its direction. Wrap the target in ref() to set them:

epg.bind(domain=ref("vmm-prod", encap="vlan-201", untagged=False))
subject.bind(filter=ref("http", directives="log"))

The attributes are validated against the relationship class at declaration time, like every other field in a design.

target

Name of the referenced object (or its raw DN under bind_dn).

Type:

str

attrs

Extra fields set on the relationship object itself.

Type:

dict[str, Any]

Cursor

Every position is a typed cursor subclass of Cursor — the makers and the set() / bind() signatures are generated per position (see the DSL reference). The base class below is the behaviour they all share.

class niwaki.design.Cursor(node)[source]

Bases: object

A position in a design tree, exposing the curated build vocabulary.

Do not instantiate directly — obtain the root cursor from niwaki.design.tenant() and children from maker calls.

property design_node: DesignNode

The underlying design node (read-only structural handle).

Deliberately verbose: short names on the cursor belong to the curated vocabulary (.node() is a maker under a route reflector or a vPC pair) — the generator enforces that no maker shadows the base cursor API.

property dn: str

Distinguished Name this node will occupy once pushed.

Returns:

DN string rooted at uni (e.g. "uni/tn-prod/BD-web").

set(**attrs)[source]

Set scalar attributes on the current object.

Values are merged with previously set attributes (last call wins) and the full attribute set is re-validated through the Pydantic model immediately — constraint violations raise at the call site, before any push.

Parameters:

**attrs (Any) – Field values using the human-readable Python names (e.g. unicast_routing=True). Sugar applies where defined (e.g. scope="vrf" on a contract).

Returns:

This cursor, for chaining.

Raises:
  • DesignError – Unknown attribute name, ACI wire name used instead of the Python field name, or attempt to change a naming prop.

  • pydantic.ValidationError – A value violates the model constraints.

Return type:

Cursor

bind(**targets)[source]

Declare lazy Rs relationships by target vocabulary and name.

Each alias=name pair records a reference resolved at push time — forward references are allowed, and the relationship class, its flavor (name vs DN) and the side it lives on are derived from REFERENCE_MAP, so .vrf("prod").bind(l3out="prod") works even though the Rs object lives on the L3Out side.

The alias must be curated on the current level — binds do not climb to an ancestor. Declare each alias on the object that owns it (.bd("b").bind(vrf="v")), so a relation can never land silently on a parent.

Parameters:

**targets (Any) – One or more alias=name pairs (e.g. vrf="prod").

Returns:

This cursor, for chaining.

Raises:

DesignError – An alias is not bindable at any level of the path.

Return type:

Cursor

bind_dn(**targets)[source]

Reference objects outside the design by raw DN (escape hatch).

Same aliases as bind(), but the value is a full DN and no closed-world lookup happens — the DN is trusted as-is and the APIC is the one to reject a dangling reference. Only aliases whose Rs class targets by DN qualify; name-flavored aliases must go through bind() (the Rs object physically stores a name, not a DN).

Parameters:

**targets (str | Ref) – One or more alias=dn pairs (e.g. vlan_pool="uni/infra/vlanns-[shared]-static"), or a ref() when the relation itself carries configuration (ref(dn, immediacy="immediate")).

Returns:

This cursor, for chaining.

Raises:

DesignError – The alias is unknown, its relation lives on the target side, or it targets by name rather than by DN.

Return type:

Cursor

provide(contract)[source]

Declare that this EPG provides contract (creates fvRsProv).

Parameters:

contract (str | Ref) – Name of a contract declared in this design, or a ref() when the relation itself carries configuration (ref("web", prio="level1")).

Returns:

This cursor, for chaining.

Return type:

Cursor

consume(contract)[source]

Declare that this EPG consumes contract (creates fvRsCons).

Parameters:

contract (str | Ref) – Name of a contract declared in this design, or a ref() when the relation itself carries configuration (ref("web", prio="level1")).

Returns:

This cursor, for chaining.

Return type:

Cursor

intra_epg(contract)[source]

Declare contract between the endpoints of this EPG (fvRsIntraEpg).

The APIC applies it to traffic that stays inside the group — the EPG must be intra-EPG isolated for it to bite.

Parameters:

contract (str | Ref) – Name of a contract declared in this design, or a ref() when the relation itself carries configuration (ref("web", prio="level1")).

Returns:

This cursor, for chaining.

Return type:

Cursor

mo(cls, **kwargs)[source]

Declare a child of an arbitrary generated class (escape hatch).

For classes outside the curated vocabulary. Naming props are picked from kwargs by name; the remainder are scalar attributes.

Parameters:
Returns:

Cursor on the new child node.

Raises:

DesignErrorcls is not a valid APIC child of this object.

Return type:

Cursor

raw(aci_class, **wire_attrs)[source]

Declare a child by ACI class name with wire attributes.

The escape hatch for classes outside the generated model set (or when only the wire spelling is at hand — a reverse import). Identity and validity come from the shipped catalogue: the class must exist, be a valid child of this node, and every naming prop of its RN format must be present in wire_attrs. Unknown wire properties fail loudly.

Parameters:
  • aci_class (str) – ACI class name (e.g. "infraKafkaPol").

  • **wire_attrs (Any) – Attributes under their wire names, values in wire form (strings, or values coercible to their wire string).

Returns:

Cursor on the new child node (base cursor — children of a raw node dispatch through raw()/mo(), not curated makers).

Raises:
Return type:

Cursor

raw_set(**wire_attrs)[source]

Set wire-named attributes on the current node (escape hatch).

For properties outside the node’s typed surface — a prop newer than the generated models, or a reverse-imported attribute with only its wire spelling at hand. The values join the emitted attributes at the wire boundary in every push mode; the strict typed model never sees the keys. Each property must exist for this class in the shipped catalogue — silence is never an option.

Returns:

self for chaining.

Raises:

DesignError – A property the catalogue does not know for this class.

Return type:

Cursor

view()[source]

Project the whole design into a frozen, walkable view.

Like push() and to_payload(), this operates on the full design tree regardless of which cursor it is called on. The view is a snapshot of the design at call time — later cursor edits do not change it; take a new one.

Returns:

A DesignView – iterate it parents-first, look nodes up by DN (view["uni/tn-prod/BD-web"]), or filter by class (view.by_class("fvBD")).

Return type:

DesignView

Example

>>> from niwaki.design import tenant
>>> cfg = tenant("prod")
>>> _ = cfg.bd("web")
>>> [node.aci_class for node in cfg.view()]
['polUni', 'fvTenant', 'fvBD']
slice(dn)[source]

Carve out a fresh design holding the subtree at dn alone.

The flagship brownfield move: “import the fabric, carve out tenant X, replay it in staging”. The result is a new, independent design — the source is never mutated — containing the subtree at dn in full, hung off its ancestor chain rebuilt as attribute-less Day-2 upserts (pushing the slice never touches an ancestor’s attributes).

References follow the wire-footprint rule (see niwaki.design._compose): a reference whose Rs object lands inside the subtree is kept — as-is when its target is inside too, pinned without the closed world otherwise (bind_dn for a DN-flavored relation, an explicit Rs child carrying the exact tn* name for a name-flavored one). An inverse edge whose Rs materialises outside the subtree is not part of this slice’s wire and is not carried.

Parameters:

dn (str) – DN of the subtree to carve ("uni/tn-prod"); "uni" copies the whole design.

Returns:

The root Cursor of the new design.

Raises:

DesignError – The design declares nothing at dn.

Return type:

Cursor

Example

>>> from niwaki.design import design
>>> cfg = design()
>>> _ = cfg.tenant("prod").bd("web")
>>> _ = cfg.tenant("dev")
>>> staged = cfg.slice("uni/tn-prod")
>>> [n.aci_class for n in staged.view()]
['polUni', 'fvTenant', 'fvBD']
to_payload()[source]

Validate, resolve references, and return the atomic push payload.

Follows the Query.build() house pattern: full inspection without execution. The returned dict is exactly what push(mode="strict") POSTs to /api/mo/uni.json.

Returns:

polUni envelope dict wrapping the whole design.

Raises:
Return type:

dict[str, Any]

external_refs()[source]

Enumerate the design’s external DN references — no transport needed.

The same enumeration push(verify_refs=True) verifies: raw-DN bind_dn targets and literal-DN makers (static_path, path_attachment, …), minus everything the design itself declares. Inspection only, like to_payload() — nothing is read or written.

Returns:

The references, sorted by (dn, referencing class).

Return type:

list[ExternalRef]

push(
client: Niwaki,
*,
mode: Literal['strict', 'staged'] = 'strict',
verify_refs: bool = False,
max_concurrent: int | None = None,
) PushReport[source]
push(
client: Niwaki,
*,
mode: Literal['plan'],
verify_refs: bool = False,
max_concurrent: int | None = None,
) PlanResult
push(
client: AsyncNiwaki,
*,
mode: Literal['strict', 'staged'] = 'strict',
verify_refs: bool = False,
max_concurrent: int | None = None,
) _Coroutine[Any, Any, PushReport]
push(
client: AsyncNiwaki,
*,
mode: Literal['plan'],
verify_refs: bool = False,
max_concurrent: int | None = None,
) _Coroutine[Any, Any, PlanResult]

Validate the design and push it through client.

Always operates on the whole design tree regardless of which cursor it is called on. Construction never touches the network — transport is injected here and only here.

Modes:
  • "strict" (default): closed-world validation (every reference must resolve inside the design), then one atomic nested POST to /api/mo/uni.json — all or nothing.

  • "staged": compile to per-object operations executed in DN-depth waves (parents before children) with a detailed report; a failing wave stops the remaining ones.

  • "plan": dry run — read the current APIC state and report what would be created or changed, pushing nothing.

Parameters:
  • client (Niwaki | AsyncNiwaki) – A connected Niwaki (returns the result directly) or AsyncNiwaki (returns an awaitable).

  • mode (PushMode) – "strict" | "staged" | "plan".

  • verify_refs (bool) – Verify the design’s external references (raw-DN bind_dn targets and literal-DN makers such as static_path) against the live APIC before anything is written — reads only. In strict/staged mode a missing or wrong-class target raises DanglingReferenceError with the complete failure list; in plan mode the per-reference statuses land on external_refs and nothing raises. Default False — the wire behavior without the flag is byte-identical to previous releases.

  • max_concurrent (int | None) – Upper bound on how many operations of one staged wave are in flight at once — async clients only. It throttles down and never up: the effective bound is the smaller of this value and the client’s own max_concurrent (default 10), so push(..., max_concurrent=50) against a default client still runs ten at a time — raise the limit on the client to go wider. Omitted, the push inherits the client’s limit, which is byte-identical to previous releases. With a sync client it is accepted and inert: the sync engine writes one object at a time. Must be >= 1, else ValueError before any I/O.

Returns:

PushReport for write modes, PlanResult for "plan" (wrapped in a coroutine for async clients).

Raises:
  • UnresolvedReferenceError – Closed-world validation failed.

  • DanglingReferenceErrorverify_refs=True in a write mode and at least one external reference cannot be honored by the APIC — nothing was written.

  • AmbiguousBindError – A bind edge has no Rs class.

  • APIError – The APIC rejected a write (strict mode).

  • StagedPushError – One or more staged operations failed — carries the partial report and the failed/skipped DNs.

Return type:

PushReport | PlanResult | _Coroutine[Any, Any, PushReport | PlanResult]

Push results

class niwaki.design.PushReport(mode, dns, request_count)[source]

Bases: object

Summary of a successful strict or staged push.

mode

The push mode that produced this report.

Type:

str

dns

The DNs this push accounts for, including the Rs objects the resolver materialises. The order is deterministic and derived from the design, never from the order in which the controller answered: a parent always precedes its descendants.

The set is mode-dependent, so never diff one mode’s report against another’s. strict walks the whole design tree, so every declared object appears. staged lists one entry per operation, and two curated kinds of class do not map one-to-one onto operations: a class marked atomic ships its whole subtree in a single request, so its children have no entry of their own, and a carrier — a path-only class the APIC materialises when a child posts beneath it — gets no operation and so no entry at all.

Type:

list[str]

request_count

Number of HTTP requests issued (1 for strict).

Type:

int

class niwaki.design.PlanResult(creates, updates, unchanged, external_refs=<factory>)[source]

Bases: object

Dry-run report of what a push would change (plan mode).

Deletions are out of scope by design: a plan never proposes removing objects that exist on the APIC but not in the design.

creates

DNs that do not exist on the APIC and would be created.

Type:

list[str]

updates

Per-DN field changes as {field: (current, desired)}.

Type:

dict[str, dict[str, tuple[Any, Any]]]

unchanged

DNs already matching the desired state.

Type:

list[str]

external_refs

Per-reference verification statuses, populated only by push(mode="plan", verify_refs=True) — plan is the warn tier and never raises for a dangling reference; each entry is a RefCheck.

Type:

list[niwaki.design._verify.RefCheck]

property has_changes: bool

True when a push would modify anything on the APIC.

class niwaki.design.ExternalRef(dn, rs_class, declared_at)[source]

Bases: object

One DN a design references without declaring it.

dn

The referenced DN, exactly as it would reach the wire.

Type:

str

rs_class

The referencing class (an Rs class for bind_dn extras, the node’s own class for literal-DN makers) — the source of the expected-class accept-set.

Type:

str

declared_at

Human-readable design path of the declaring node, for error messages.

Type:

str

class niwaki.design.RefCheck(ref, status, expected, found, detail='', apic_code=None)[source]

Bases: object

The verification outcome for one external reference.

ref

The reference that was checked.

Type:

niwaki.design._verify.ExternalRef

status

ok (target exists and matches the accept-set), missing (no object at the DN), wrong_class (an object exists but its class is outside the referencing class’s accept-set), unverifiable (the expected class is a carrier — a GET answers empty even for a valid path), or error (the read itself failed; see detail).

Type:

Literal[‘ok’, ‘missing’, ‘wrong_class’, ‘unverifiable’, ‘error’]

expected

The accept-set the target was checked against (empty when unknown — existence-only check).

Type:

tuple[str, …]

found

The wire class actually found at the DN, or None.

Type:

str | None

detail

Read-error text for status="error", else "".

Type:

str

apic_code

The APIC’s own error code behind a status="error", when the failure came from the controller and carried one. None for every other status, and for a read that failed without an APIC error envelope — the HTML page a loaded controller answers instead of JSON, say. (A client-side timeout is not one of these: it raises a TransportError, which this pass does not catch, so it aborts the verification rather than becoming a row.) detail stays the human text — this is the machine-readable half, so a caller can tell “the DN is malformed” from “I am not allowed to look”.

Type:

str | None

Reverse import

The inverse of push(): rebuild a design from a Snapshot — the whole fabric or any scope under uni — or from a raw payload envelope, preferring the curated vocabulary (makers, bind(), the contract verbs) and falling back to the wire-name escape hatches so the design compiles to the same wire payload the source describes.

niwaki.design.to_design(snapshot, *, on_unknown='raise', redacted='raise')[source]

Rebuild a design from a snapshot — the inverse of push.

Walks the snapshot tree and re-expresses every captured object in the design DSL, preferring the curated vocabulary (makers, bind(), the contract verbs) and falling back to the wire-name escape hatches (raw() / raw_set) whenever a curated inversion cannot be made provably equivalent. The returned design compiles to the same wire payload the snapshot describes: pushing it in mode="plan" against the fabric the snapshot was taken from reports no changes.

A live snapshot carries every configurable property at its current wire value, unset markers included; the importer normalises them so the strict typed models never see what the fabric spells as “not configured”: empty strings drop (the design layer cannot emit "" by contract), a model-refused value equal to the property’s schema default drops (vmac="not-applicable", vrfIndex="0"), and any other model-refused value rides the wire channel verbatim (raw_set — the APIC served it, the wire is authoritative).

Objects whose DN itself carries a secret (listed in warnings) import unchanged — their identity cannot be redacted; review the snapshot’s warnings before sharing anything derived from it.

Parameters:
  • snapshot (Snapshot) – A Snapshot — the whole configuration (scope "uni") or any narrower config scope under it ("uni/tn-prod", "uni/infra", …). For a scoped snapshot the ancestors of the captured object are rebuilt from the scope DN as attribute-less Day-2 upserts: pushing the design never touches an attribute the capture does not carry.

  • on_unknown (Literal['raise', 'raw']) – Policy for classes/properties outside this SDK’s schema baseline (a snapshot from a newer firmware). "raise" (default) collects every occurrence into one SnapshotImportError; "raw" carries them verbatim on the wire-attribute channel instead.

  • redacted (Literal['raise', 'skip']) – Policy for values holding the REDACTED sentinel (curated secrets are elided at capture). "raise" (default) collects them — a design pushing the sentinel literally would be wrong; "skip" drops those values from the design. A relation whose target value was redacted imports as an unformed relation under "skip" — re-declare it with the real target before pushing anywhere.

Returns:

The root Cursor of the rebuilt design (a polUni node), ready for to_payload() / push().

Raises:
  • DesignError – The snapshot’s scope is outside uni, its tree is empty, a "uni" snapshot is not rooted on polUni, or an ancestor segment of a scoped snapshot cannot be resolved.

  • SnapshotImportError – One or more items could not be imported — collected over the whole tree, never first-fail. The partial design is discarded.

Return type:

Cursor

Example:

from niwaki import snapshot
from niwaki.design import to_design

snap = snapshot.Snapshot.from_json(Path("fabric.json").read_text())
config = to_design(snap)
result = config.push(aci, mode="plan")
assert not result.has_changes   # the design IS the fabric
niwaki.design.from_payload(payload, *, on_unknown='raise', redacted='raise')[source]

Rebuild a design from a raw APIC payload — the inverse of to_payload.

Accepts the atomic polUni envelope shape ({"polUni": {"attributes": …, "children": […]}}) — what to_payload() emits, what a strict push POSTs, and what configuration JSON exported from other tooling commonly looks like. The payload is converted to the snapshot tree shape (RNs recomputed from the catalogue, fail-loud on a missing naming value or a status write directive) and handed to the same importer as to_design() — one inversion engine, two doors.

Parameters:
Returns:

The root Cursor of the rebuilt design.

Raises:
  • DesignError – The envelope is malformed — not rooted on polUni, several class keys in one envelope, an unknown class, a missing naming value, or a status directive.

  • SnapshotImportError – See to_design().

Return type:

Cursor

Example:

from niwaki.design import from_payload, tenant

original = tenant("prod")
original.bd("web")
clone = from_payload(original.to_payload())
assert clone.to_payload() == original.to_payload()
class niwaki.design.ImportProblem(dn, kind, detail)[source]

Bases: object

One snapshot item to_design() cannot import.

dn

DN of the offending object (reconstructed from the snapshot tree).

Type:

str

kind

Problem family — "unknown-class", "unknown-property", "redacted-value", "invalid-value" or "structure".

Type:

Literal[‘unknown-class’, ‘unknown-property’, ‘redacted-value’, ‘invalid-value’, ‘structure’]

detail

Human-readable description naming the class/property/value.

Type:

str

Walking a design

class niwaki.design.DesignView(root)[source]

Bases: object

A whole design, frozen and walkable.

Obtain one from niwaki.design.Cursor.view(). Iteration yields every node parents-first in declaration order (the polUni root included); lookups are by DN.

Example:

from niwaki.design import tenant

cfg = tenant("prod")
cfg.bd("web").bind(vrf="main")
cfg.vrf("main")

view = cfg.view()
[n.aci_class for n in view]
# → ['polUni', 'fvTenant', 'fvBD', 'fvCtx']
view["uni/tn-prod/BD-web"].binds[0].alias   # → 'vrf'
[n.dn for n in view.by_class("fvCtx")]      # → ['uni/tn-prod/ctx-main']
property root: DesignViewNode

The polUni root node of the design.

get(dn)[source]

The node at dn, or None when the design does not declare it.

by_class(aci_class)[source]

Every declared node of aci_class, in declaration order.

class niwaki.design.DesignViewNode(dn, aci_class, label, position, naming, attrs, raw_attrs, binds, children)[source]

Bases: object

One declared object of a design, projected read-only.

dn

The DN this object will occupy once pushed.

Type:

str

aci_class

Wire class name ("fvBD").

Type:

str

label

The maker that created the node ("bd"), or the class name for escape-hatch nodes (.mo() / .raw()).

Type:

str

position

Dotted maker path from the polUni root ("tenant.bd"), "" for the root itself, None for nodes outside the curated vocabulary.

Type:

str | None

naming

Naming property values. Readable names for typed nodes; wire names for catalogue-served raw() nodes (their whole surface is wire-spelled).

Type:

dict[str, Any]

attrs

Non-naming attributes exactly as declared (readable names, pre-coercion values).

Type:

dict[str, Any]

raw_attrs

Wire-channel escapes (raw_set() and the reverse importer’s escapes) — wire names, wire string values.

Type:

dict[str, str]

binds

The references declared on this node, in declaration order.

Type:

tuple[niwaki.design._view.DesignViewBind, …]

children

Child nodes, in declaration order.

Type:

tuple[niwaki.design._view.DesignViewNode, …]

class niwaki.design.DesignViewBind(kind, alias, target_class, target, attrs)[source]

Bases: object

One reference declared on a design node, as the caller declared it.

kind

"bind" (closed-world alias), "bind_dn" (raw-DN escape) or "verb" (curated contract verb such as provide).

Type:

Literal[‘bind’, ‘bind_dn’, ‘verb’]

alias

The vocabulary word used at the call site ("vrf", "provide", …).

Type:

str

target_class

ACI class the reference points at — possibly abstract for curated aliases ("infraDomP").

Type:

str

target

The target’s primary name — or its raw DN for "bind_dn".

Type:

str

attrs

Configuration carried by the relation itself (ref() attributes), readable names. Empty for a pure edge.

Type:

dict[str, Any]

Composition

Carving is a cursor method — Cursor.slice("uni/tn-prod") returns a fresh design holding that subtree over an attribute-less ancestor chain; recombining is a function:

niwaki.design.merge(*designs)[source]

Combine several designs into one — fail-loud on any contradiction.

The union is by DN: an object declared in one source is carried whole; an object declared in several must agree — same class, and no attribute (typed or wire-channel) set to two different values. References concatenate, identical duplicates collapse (the resolver already treats two identical relations as one). Every contradiction across the whole merge is collected before raising, never first-fail.

Parameters:

*designs (Cursor) – Two or more design cursors (any cursor of each design — the whole tree is taken, like push). Sources are never mutated.

Returns:

The root Cursor of a fresh merged design.

Raises:
  • DesignError – Fewer than two designs given.

  • MergeConflictError – At least one contradiction — carries every conflicting (dn, what, values) triple.

Return type:

Cursor

Example:

from niwaki.design import merge, tenant

base = tenant("prod")
base.bd("web")
overlay = tenant("prod")
overlay.bd("web").set(unicast_routing=True)
combined = merge(base, overlay)

Emitting code

niwaki.design.to_code(source, *, var='cfg')[source]

Emit the Python DSL source that replays source — the code emitter.

The inverse of executing a design script: any design — hand-built, reverse-imported (to_design()), composed (slice() / merge()) — renders as reviewable, replayable Python. Executing the emitted source yields a design whose payload is canonically byte-identical to the source’s; the acceptance suite pins exactly that round trip.

Parameters:
  • source (Cursor | DesignView) – A design cursor (any cursor — the whole design is taken, like push) or an already-taken DesignView.

  • var (str) – Name of the root variable in the emitted source (default "cfg") — the emitted script ends with that variable holding the root cursor.

Returns:

Python source text – imports first, then the declarations in the design’s own order. Curated positions render as their makers and references; everything outside the curated vocabulary renders through the wire-name doors (raw() / raw_set()) — including the tag/annotation objects that fabrics touched by other tooling always carry.

Return type:

str

Example:

from niwaki.design import tenant, to_code

cfg = tenant("prod")
cfg.bd("web").bind(vrf="main")
cfg.vrf("main")
print(to_code(cfg))
# cfg = design()
# tenant_prod = cfg.tenant('prod')
# tenant_prod.bd('web').bind(vrf='main')
# tenant_prod.vrf('main')

Reconciliation

The other half of drift, beside plan: what the fabric carries that the design does not declare. Read-only — nothing is ever proposed for deletion.

niwaki.design.reconcile(source, client)[source]

Report what the fabric carries under this design’s domains and the design does not declare — the other half of drift, beside plan.

One snapshot capture of the whole configuration (the same 15-or-so requests a backup costs, whatever the design’s size) is walked against the design’s own DNs, resolved references included. Only the domains the design declares (its direct children of polUni, carriers included) are accounted — a domain the design does not claim is nobody’s drift.

Reads only; nothing is ever written, and nothing is proposed for deletion — a design never removes what it does not declare. Sync only, like niwaki.snapshot.take() which it reuses.

Parameters:
  • source (Cursor) – Any cursor of the design (the whole tree is taken, like push).

  • client (Niwaki) – A connected Niwaki.

Returns:

A Reconciliation. clean is True when the fabric holds nothing created beyond the design; fabric-materialised objects (default relations, minted containers — non-creatable classes) report separately under implicit.

Return type:

Reconciliation

Example:

from niwaki import snapshot
from niwaki.design import reconcile, to_design

cfg = to_design(snapshot.take(aci, "uni"), redacted="skip")
report = reconcile(cfg, aci)
assert report.clean   # an imported design covers its own fabric

# A partial hand design instead names what it does not own:
for dn in report.orphan_subtrees:
    print("not mine:", dn)
class niwaki.design.Reconciliation(extra, implicit, orphan_subtrees)[source]

Bases: object

The fabric-side half of drift — objects the design does not declare.

extra

Every live, creatable object under the design’s declared domains that the design does not declare, as sorted (dn, aci_class) pairs — things someone created.

Type:

list[tuple[str, str]]

implicit

The undeclared objects whose class the schema marks non-creatable — fabric-materialised state (default relations, minted containers), never an operator’s leftover. Reported for completeness, excluded from clean.

Type:

list[tuple[str, str]]

orphan_subtrees

The minimal roots of the extra regions — the extra objects whose parent is declared (or implicit, or a domain root). The operator-granularity answer to “which whole subtrees are not mine?”.

Type:

list[str]

property clean: bool

True when the fabric carries nothing created beyond the design.