Skip to main content

Modern, typed, design-first Python SDK for Cisco ACI (APIC)

Project description

niwaki — a cloud-pruned garden tree

NIWAKI 庭木

Cisco ACI for humans — describe, push, observe.

The modern, typed, design-first Python SDK for Cisco ACI.

cisco aci network automation pydantic python license


Niwaki (庭木) is the Japanese art of sculpting full-size, living garden trees — exactly what this SDK does to the APIC Management Information Tree: not a miniature in a pot, a production tree, pruned with intent.

The promise: you should not have to memorise the APIC object model. Navigation, object names, and attributes use operator vocabulary with full IDE autocompletion; the SDK translates to ACI classes, tn*Name relation props, and wire attribute names for you.

Why another SDK for Cisco ACI?

Cisco ships an official Python SDK — cobra. It is authoritative and complete, but it was designed a decade ago and it shows: you install it as two wheels downloaded from your own APIC, version-matched to the firmware, targeting "Python 2.7 or 3.6"; you write wire names and relation classes by hand (RsCtx(bd, tnFvCtxName='prod')); and every mistake is discovered by the APIC, after the POST.

cobra (official) niwaki
Distribution two .whl downloaded from a running APIC, firmware-matched one wheel, standard packaging, installable from an index
Python "2.7 or 3.6", untyped 3.12+, Typing :: Typed, full IDE autocompletion
Writing model imperative: build MOs, ConfigRequest, commit() design-first: describe → plan → push (atomic or staged waves)
Vocabulary ACI classes and wire names (fv.BD, arpFlood, tnFvCtxName) operator verbatim (.bd("web").set(arp_flooding=True).bind(vrf="prod"))
References relation classes + target-name strings, unchecked bind() resolved closed-world at push time — a typo fails before any request, with a did-you-mean
Validation server-side, after the POST at the call site (Pydantic), plus a plan dry-run diff
Async / retry / pagination first-class async mirror, proactive token refresh, retries, transparent pagination

cobra remains the reference when you need guaranteed write parity with your exact firmware and Cisco support behind it. For everything else — reading fabrics, building and converging configuration as code — niwaki is built to be the SDK you want to write. The deep comparison lives in the documentation.

Installation

uv add niwaki          # or: pip install niwaki

Requires Python 3.12+. To work on the SDK itself, clone and uv sync --extra dev.

Restricted network? Every GitHub Release ships an offline wheelhouse (niwaki + all dependencies as wheels, with checksums and provenance attestations) for environments that cannot reach PyPI — the step-by-step is in the installation guide.

Full documentation — guides, cookbook, vocabulary book, API reference — lives at https://k3l0-dev.github.io/niwaki/.

Quickstart — declarative provisioning (design DSL)

One mental model: describe the desired configuration with the design DSL, apply it with push(), observe with the facade. The DSL covers the whole uni subtree — tenants, access policies (infra), fabric policies (fabric), controller policies — with the same vocabulary everywhere.

Build a detached design tree (no session, no I/O), then validate and push it in one call:

from niwaki import Niwaki
from niwaki.design import tenant

config = (
    tenant("prod")
    .app("shop")
        .epg("frontend").bind(bd="frontend").consume("fe-to-be")
        .epg("backend").bind(bd="backend").provide("fe-to-be")
    .bd("frontend")
        .set(unicast_routing=True)
        .subnet("10.0.1.1/24")
        .bind(vrf="prod")
    .bd("backend")
        .set(unicast_routing=True)
        .subnet("10.0.2.1/24")
        .bind(vrf="prod")
    .vrf("prod")
    .filter("api")
        .entry("rest", tcp=8080)
    .contract("fe-to-be")
        .set(scope="vrf")
        .subject("api").bind(filter="api")
)

with Niwaki("https://apic.example.com", "admin", "secret") as aci:
    config.push(aci, mode="strict")

Fabric and access policies use the same verbs — and multi-domain designs are one design() away:

from niwaki.design import design

cfg = design()
cfg.fabric().datetime_policy("prod-ntp").ntp_provider("10.0.0.1")
inf = cfg.infra()
inf.vlan_pool("prod", "static").range("vlan-100", "vlan-199")
cfg.phys_dom("prod-phys").bind(vlan_pool="prod")
inf.aaep("prod-aaep").bind(domain="prod-phys")
cfg.tenant("prod").app("shop").epg("web").bind_dn(domain="uni/phys-prod-phys")

cfg.push(aci)          # everything above in ONE atomic POST

Day-2 changes are just smaller designs — declare the field you want, the parent chain rides along as attribute-less upserts:

from niwaki.design import infra

flip = infra().cdp_policy("cdp-on", admin_state="disabled")
flip.push(aci, mode="plan")   # shows exactly one field change
flip.push(aci)

What the DSL gives you:

  • Structure is literal, vocabulary is translated — every maker maps 1:1 to a real APIC child class (.subject() is a vzSubj, .pim() is a pimCtxP), but names and parameters are the ones operators actually use (entry("rest", tcp=8080) compiles to etherT/prot/dFromPort/dToPort).
  • Lazy, closed-world referencesbind(), provide(), consume() resolve at push time; forward references are fine; a typo fails before any request, with a did-you-mean. Direction is handled for you: .vrf("prod").bind(l3out="ext") creates the l3extRsEctx on the L3Out side, where ACI expects it.
  • Typed cursors per position — makers, set() fields, and bind() aliases are generated with full signatures: autocompletion and mypy cover the entire curated vocabulary. .mo(AnyClass, ...) remains as the escape hatch, and bind_dn(alias=dn) references objects outside the design by raw DN.
  • Eager validation — every name and attribute is checked by the Pydantic models at the call site, not on the wire.

Push modes

Mode Behaviour
strict (default) Closed-world validation, then one atomic POST of the whole design to /api/mo/uni.json — all or nothing.
staged One operation per object, executed in DN-depth waves (parents before children); atomic classes (vPC pairs) ship whole; a partial failure raises StagedPushError with plain DNs.
plan Dry run: reads the current APIC state (one read per declared domain) and reports creates/updates — nothing is pushed.

config.to_payload() returns the exact strict-mode payload without executing anything (same philosophy as Query.build()).

Reading — typed queries

from niwaki import Niwaki
from niwaki.models.fv.fvBD import fvBD

with Niwaki("https://apic.example.com", "admin", "secret") as aci:
    # Jargon navigation, no class imports needed
    bd = aci.tenant("prod").bd("frontend").read()

    # Query builder: filters, scoping, enrichment, pagination
    # (filters address the APIC attribute names — the wire side)
    bds = aci.query(fvBD).where(arpFlood=True).under("uni/tn-prod").fetch()
    n = aci.tenant("prod").query(fvBD).count()

    # Any of the ~15k APIC classes by name (read-only/operational included)
    nodes = aci.query("topSystem").naming_only().fetch()

Async is a first-class mirror of the sync API:

from niwaki import AsyncNiwaki
from niwaki.models.fv.fvTenant import fvTenant

async with AsyncNiwaki("https://apic.example.com", "admin", "secret") as aci:
    tenants, bd = await aci.gather(
        aci.query(fvTenant).fetch(),
        aci.tenant("prod").bd("frontend").read(),
    )
    await config.push(aci, mode="strict")   # the design DSL is async-ready too

What's inside

  • Design DSL (niwaki.design): THE write path — see above. Curated vocabulary in domain/vocabulary.yaml, typed cursors generated per position, unified reference resolver (REFERENCE_MAP, name + DN flavors, abstract targets).
  • 2,222 generated Pydantic models (APIC v6.0 schemas) with human-readable field names, constraints, and 558 enums — models carry data and validation, never write logic.
  • Facade (observation): jargon navigation (aci.tenant("x").bd("y")), typed reads, queries, delete.
  • Sync + async transport: cookie/token auth, proactive refresh, retry with backoff, transparent pagination, typed exception hierarchy.
  • Cold-start import: ~90 ms; heavy tables load lazily on first use.

Development

uv sync --extra dev

Documentation (the hosted site is https://k3l0-dev.github.io/niwaki/; to build it locally — static HTML, no server needed):

uv sync --extra docs
bash scripts/docs.sh open     # build + open docs/_build/html/index.html

The full test suite ships with the repository — 14,200+ unit tests plus the executable documentation (every python fence in docs/ runs as a test):

uv run pytest --ignore=tests/integration tests/ docs/

The integration suite (a three-act live walkthrough) needs a lab APIC and skips itself without one. Release engineering runs in the maintainers' private infrastructure; this repository is the public home of the SDK: source, tests, documentation, releases and issues.

Status

Active development. 14,200+ tests, mypy strict, ruff. The design DSL covers a curated vocabulary across tenant, access (infra), fabric, and controller policies — 175+ curated positions, growing wave by wave; everything else is reachable via .mo() and bind_dn(). Why this SDK exists: the comparison with cobra.

License

Apache License 2.0 — see LICENSE and NOTICE. Copyright 2026 Monark AIOPS SRL. Developed by Khalid El-Ouiali.

Cisco, Cisco ACI and APIC are trademarks of Cisco Systems, Inc. niwaki is an independent project, not affiliated with or endorsed by Cisco Systems, Inc.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

niwaki-0.5.0.tar.gz (1.1 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

niwaki-0.5.0-py3-none-any.whl (3.3 MB view details)

Uploaded Python 3

File details

Details for the file niwaki-0.5.0.tar.gz.

File metadata

  • Download URL: niwaki-0.5.0.tar.gz
  • Upload date:
  • Size: 1.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for niwaki-0.5.0.tar.gz
Algorithm Hash digest
SHA256 057bbcbaece6e474d15ec1066658354b2ae353fc7f3186b22505795267ffed9a
MD5 ebe57fd44abf574ef3e41242a5407eb6
BLAKE2b-256 e2ed78c24770bdb39de3eb619b31a387add98d5acda44293946dd16181f9ac87

See more details on using hashes here.

Provenance

The following attestation bundles were made for niwaki-0.5.0.tar.gz:

Publisher: release.yml on k3l0-dev/niwaki

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file niwaki-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: niwaki-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 3.3 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for niwaki-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 78f7ca71c2116ce5dd133ddf5543752f13fbcf65e45e019c36066f0ccf410dbe
MD5 7dc2ccf42e166323624472d7873405a6
BLAKE2b-256 7840c3d282de9fb3dddcfeabd142c77f6efb01a3d4a591424e3620dc2ebdfdaa

See more details on using hashes here.

Provenance

The following attestation bundles were made for niwaki-0.5.0-py3-none-any.whl:

Publisher: release.yml on k3l0-dev/niwaki

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page