Modern, typed, design-first Python SDK for Cisco ACI (APIC)
Project description
NIWAKI 庭木
Cisco ACI for humans — describe, push, observe.
The modern, typed, design-first Python SDK for Cisco ACI.
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 vocabulary (.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
config = design()
config.fabric().datetime_policy("prod-ntp").ntp_provider("10.0.0.1")
inf = config.infra()
inf.vlan_pool("prod", "static").range("vlan-100", "vlan-199")
config.phys_dom("prod-phys").bind(vlan_pool="prod")
inf.aaep("prod-aaep").bind(domain="prod-phys")
config.tenant("prod").app("shop").epg("web").bind_dn(domain="uni/phys-prod-phys")
config.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 avzSubj,.pim()is apimCtxP), but names and parameters are the ones operators actually use (entry("rest", tcp=8080)compiles toetherT/prot/dFromPort/dToPort). - Lazy, closed-world references —
bind(),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 thel3extRsEctxon the L3Out side, where ACI expects it. - Typed cursors per position — makers,
set()fields, andbind()aliases are generated with full signatures: autocompletion and mypy cover the entire curated vocabulary..mo(AnyClass, ...)remains as the escape hatch, andbind_dn(alias=dn)references objects outside the design by raw DN. - Cisco's definitions in every hover — the APIC schema comments flow into field descriptions, enum values, and maker signatures: your IDE documents ACI while you type.
- 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 indomain/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): vocabulary 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 — every curated position is listed in the generated coverage matrix; 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file niwaki-0.6.0.tar.gz.
File metadata
- Download URL: niwaki-0.6.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1eec7abd890e56c08087994d4555a770da5d7ed4ecedfb2f7276d49a69cb12f1
|
|
| MD5 |
3839695dc0d5711f6d156ff9e0d118ae
|
|
| BLAKE2b-256 |
c6842396c794a3436fb8b82bf23f8c39092212611ec1700ce0445a294590621f
|
Provenance
The following attestation bundles were made for niwaki-0.6.0.tar.gz:
Publisher:
release.yml on k3l0-dev/niwaki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
niwaki-0.6.0.tar.gz -
Subject digest:
1eec7abd890e56c08087994d4555a770da5d7ed4ecedfb2f7276d49a69cb12f1 - Sigstore transparency entry: 2163374446
- Sigstore integration time:
-
Permalink:
k3l0-dev/niwaki@01bd3d0e46788fef0fa42ec1c66e2a32633852e2 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/k3l0-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@01bd3d0e46788fef0fa42ec1c66e2a32633852e2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file niwaki-0.6.0-py3-none-any.whl.
File metadata
- Download URL: niwaki-0.6.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
04db05578235304330586ac980dc90ae9a78e7689e6a5a7729e72b8a290e70a7
|
|
| MD5 |
e0650eaca80eeb3c445ae07b5b23b6b4
|
|
| BLAKE2b-256 |
7c10665bc2eb17b76054ac6e99d138737cd57411a5bcf72300c91ce1a68f6275
|
Provenance
The following attestation bundles were made for niwaki-0.6.0-py3-none-any.whl:
Publisher:
release.yml on k3l0-dev/niwaki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
niwaki-0.6.0-py3-none-any.whl -
Subject digest:
04db05578235304330586ac980dc90ae9a78e7689e6a5a7729e72b8a290e70a7 - Sigstore transparency entry: 2163374516
- Sigstore integration time:
-
Permalink:
k3l0-dev/niwaki@01bd3d0e46788fef0fa42ec1c66e2a32633852e2 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/k3l0-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@01bd3d0e46788fef0fa42ec1c66e2a32633852e2 -
Trigger Event:
push
-
Statement type: