Working with the models¶
niwaki ships 2,222 typed Pydantic models generated from the APIC schemas, plus 676 enums. Models carry data and validation only — writing is the design DSL’s job (The design DSL), reading returns them fully typed (Observing the fabric). This page is about handling them directly.
Importing a class¶
Every model lives in a package named after its ACI class prefix, and the import path spells the ACI class name — if you know the class, you know the import:
from niwaki.models.fv.fvBD import fvBD
from niwaki.models.fv.fvTenant import fvTenant
from niwaki.models.vz.vzBrCP import vzBrCP
Most of the time you do not import at all:
navigation returns typed instances without imports —
aci.tenant("prod").bd("web").read()is anfvBD;queries by name accept any of the ~15,000 class strings —
aci.query("topSystem")— including operational classes outside the generated set.
Import a class when you want the type itself: typed queries
(aci.query(fvBD)), isinstance checks, or the .mo() escape hatch.
Readable fields, wire aliases¶
Every attribute has a human-readable field name; the APIC wire name rides along as its alias. You write the readable one, the wire format is produced at serialisation:
bd = fvBD(name="web", arp_flooding=True)
assert bd.arp_flooding is True
assert bd.to_apic() == {"fvBD": {"attributes": {"name": "web", "arpFlood": "true"}}}
Two places still speak wire names, because the APIC does: query filters
(where(arpFlood=True) — the filter string goes to the APIC verbatim) and
raw payloads you inspect with to_payload().
Enums¶
Constrained attributes are real StrEnums — one module per enum, same
import convention as the models:
from niwaki.models.enums.FvBDType import FvBDType
bd = fvBD(name="storage", type="fc") # plain strings coerce…
assert bd.type is FvBDType.FC # …into the enum member
assert list(FvBDType) == [FvBDType.FC, FvBDType.REGULAR]
Validation at the call site¶
Values are validated the moment you construct — a bad value never reaches the wire:
import pydantic
try:
fvBD(name="web", type="banana")
except pydantic.ValidationError as exc:
print(exc.error_count(), "error —", exc.errors()[0]["loc"])
Instances returned by reads keep every APIC attribute (also the ones outside the generated schema version), so a round-trip never loses data.
Cisco’s definitions, built in¶
The APIC schemas carry a human-written definition for most configurable
properties — and the generated models keep them. Every described field
carries it as its Pydantic description, so your IDE shows Cisco’s own
words when you hover an attribute, and the DSL reference renders them for
every curated position (Vocabulary book):
from niwaki.models.fv.fvBD import fvBD
info = fvBD.model_fields["arp_flooding"]
assert "ARP flooding" in info.description
Enum values are documented too — each generated StrEnum member carries
Cisco’s per-value description as an attribute docstring (OspfNwT.BCAST →
“Broadcast interface”), which IDEs surface in autocompletion.
The .mo() escape hatch¶
The design DSL’s curated vocabulary covers 927 positions; the other 2,000+
classes remain one call away. .mo(Class, **attrs) declares a child of any
generated class at the current position — containment is still validated
against the schema:
from niwaki.design import tenant
from niwaki.models.mon.monEPGPol import monEPGPol
config = tenant("prod")
config.mo(monEPGPol, name="strict-monitoring")
payload = config.to_payload()
children = payload["polUni"]["children"][0]["fvTenant"]["children"]
assert children == [{"monEPGPol": {"attributes": {"name": "strict-monitoring"}}}]
If you reach for .mo() often for the same class, that is a vocabulary gap
worth reporting — the vocabulary request issue template exists exactly for
that (coverage matrix).
Next steps¶
Vocabulary book — every field of every curated class
The design DSL — the write path built on these models