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 in the Python 2 era
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)
.bind(vrf="prod")
.subnet("10.0.1.1/24")
.bd("backend")
.set(unicast_routing=True)
.bind(vrf="prod")
.subnet("10.0.2.1/24")
.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. - References that configure the relationship — some ACI relations carry
fields of their own (the immediacy of a domain attachment, the
directivesof a filter under a subject — contract logging lives there).ref()sets them without leaving the closed world:subject.bind(filter=ref("web", directives="log")). - 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 only what the design declares 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:
# Vocabulary 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
Features
Design-first configuration — one way to write, three ways to apply.
- Describe the whole
unisubtree — tenants, access policies (infra), fabric policies (fabric), controller, AAA — with one operator vocabulary and full IDE autocompletion. Structure is literal (every maker maps 1:1 to a real APIC child class), vocabulary is translated (entry("rest", tcp=8080)compiles toetherT/prot/dFromPort/dToPort). - Three push modes:
strict(one atomic POST of the whole design),staged(dependency-ordered waves, a partial failure raisesStagedPushErrorwith plain DNs),plan(dry-run diff — nothing is pushed).to_payload()inspects the exact payload without executing. - 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. Relation direction is handled for you. - References that configure the relationship —
ref()sets the fields some ACI relations carry (domain-attachment immediacy, thedirectivesof a filter under a subject = contract logging).bind_dn()references objects outside the design by raw DN;.mo(AnyClass, ...)is the escape hatch to any class. - Day-2 changes are just smaller designs — declare the field you want and the parent chain rides along as attribute-less upserts.
Typed models and vocabulary.
- 2,222 Pydantic models generated from APIC v6.0 schemas, with human-readable
field names (
arp_flooding↔ wirearpFlood), schema constraints and 676 enums. Models carry data + validation only, never write logic. - A complete type system: named numbers (port
80↔"http"), bitmask flags, validated IP/MAC addresses, and wire-name aliases that stay visible to the type-checker (PEP 681). Validation is eager, at the call site — never "discover it on the wire". - Typed cursors per position — makers,
set()fields andbind()aliases are generated with full signatures, so autocompletion and mypy cover the entire curated vocabulary. Cisco's own schema comments flow into every hover.
Reading and observing.
- Query builder: filters, scoping (
under,children), enrichment (with_faults/health/stats),naming_only/config_only, sorting, pagination;build()to inspect. Any of ~15k APIC classes by name. - Observation facade: vocabulary navigation (
aci.tenant("x").bd("y").read()), typed reads, queries, delete.
Transport and packaging.
- Sync + async mirrors: cookie/token auth with proactive refresh, retries with
backoff, transparent pagination, a typed exception hierarchy, and
gather()for concurrent reads. - One wheel, standard packaging, installable from any index;
Typing :: Typed. Every GitHub Release ships an offline wheelhouse (all dependencies, checksums, provenance attestations) for air-gapped networks. Imports stay lightweight — heavy tables load lazily on first use.
Validated on a live fabric
The design-first configuration surface is proven against a live Cisco APIC. An exhaustive integration suite provisions a real controller (or the APIC simulator) through the SDK — eight domain walkthroughs (fabric/access, fabric, tenant, contracts, L3Out/L2Out, service graphs, observability, management) that sweep the configuration surface in depth: every enum value, the combinations of interacting fields, and every curated child of every parent, with mutually-exclusive settings factored across separate tenants and VRFs so both sides of each exclusion are covered. The controller accepts what the SDK produces, and each declared object is confirmed live with the attributes and children its design declared.
You can reproduce every one of these on your own lab. Point the suite at your fabric and run it in a single pass:
uv run pytest tests/integration -m integration -s
The suite reads your APIC from a .env file and skips when no fabric is
reachable, so it is always safe to run. The step-by-step method — install, point
at your fabric, run the whole suite or one phase, verify what landed, and clean
up — is in
tests/integration/README.md.
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 — the unit suite 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 live walkthrough against a lab APIC) skips itself when no fabric is reachable. Release engineering runs in the maintainers' private infrastructure; this repository is the public home of the SDK: source, tests, documentation, releases and issues.
Status
Actively developed — the changelog
says what each release brings. Typed end to end (mypy strict), linted and
formatted (ruff). The design DSL covers a curated vocabulary across tenant,
access (infra), fabric, and controller policies — the generated
coverage matrix
lists every position; everything else stays 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-1.2.0.tar.gz.
File metadata
- Download URL: niwaki-1.2.0.tar.gz
- Upload date:
- Size: 14.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6c7d6265b38df4da571a1f61d6752d8b7bc6236a546815f29edcfd34d35a82c
|
|
| MD5 |
de6549637562e0c214fc93e43e2f1fef
|
|
| BLAKE2b-256 |
8d165eb5f3f1cc60f55db8318f38c4a0fc1843fa631e947374681cfcc038e4a1
|
Provenance
The following attestation bundles were made for niwaki-1.2.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-1.2.0.tar.gz -
Subject digest:
d6c7d6265b38df4da571a1f61d6752d8b7bc6236a546815f29edcfd34d35a82c - Sigstore transparency entry: 2205121332
- Sigstore integration time:
-
Permalink:
k3l0-dev/niwaki@94a772afa817f371529b7bf1fb11f708868f9cc1 -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/k3l0-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@94a772afa817f371529b7bf1fb11f708868f9cc1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file niwaki-1.2.0-py3-none-any.whl.
File metadata
- Download URL: niwaki-1.2.0-py3-none-any.whl
- Upload date:
- Size: 16.1 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 |
9e927920dea44bd8d705e44273c25bbee1ee914bdef3ad043fcc64620ede5d88
|
|
| MD5 |
2f76d1a720f5c0167e70820c44b0bf77
|
|
| BLAKE2b-256 |
9e36a9cf48878c27d97b2c07a1a01302c35c8f8bc41e0a678639d6d6c58fd4f2
|
Provenance
The following attestation bundles were made for niwaki-1.2.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-1.2.0-py3-none-any.whl -
Subject digest:
9e927920dea44bd8d705e44273c25bbee1ee914bdef3ad043fcc64620ede5d88 - Sigstore transparency entry: 2205121357
- Sigstore integration time:
-
Permalink:
k3l0-dev/niwaki@94a772afa817f371529b7bf1fb11f708868f9cc1 -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/k3l0-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@94a772afa817f371529b7bf1fb11f708868f9cc1 -
Trigger Event:
push
-
Statement type: