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.4.0.tar.gz (938.9 kB view details)

Uploaded Source

Built Distribution

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

niwaki-0.4.0-py3-none-any.whl (2.9 MB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for niwaki-0.4.0.tar.gz
Algorithm Hash digest
SHA256 dd63cec76168a32ace6474cda0f1c934eb3eceec1744df94a3d7916a546d387c
MD5 b929fefe68f83cd5b6dd315fa2ed4d3f
BLAKE2b-256 bf3eaa23b18da4644182b0461938211d357b4269af6d3d765113eec4ed03c04a

See more details on using hashes here.

Provenance

The following attestation bundles were made for niwaki-0.4.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.4.0-py3-none-any.whl.

File metadata

  • Download URL: niwaki-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 2.9 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.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 40717488d15e57d6384fb2dbd3a480117a636f39fa7a1963123228f34c256741
MD5 7d2c243b4fe2f61354bbdde0cf673302
BLAKE2b-256 09f82398a2783920a700ed1063b75852af7b31a9d80d5e14db5f778614dc291b

See more details on using hashes here.

Provenance

The following attestation bundles were made for niwaki-0.4.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