niwaki.exceptions

Niwaki SDK — public exception hierarchy.

All errors raised by the SDK are subclasses of NiwakiError so callers can write a single broad except NiwakiError or target a specific branch:

from niwaki.exceptions import (
    NiwakiError,          # catch-all
    AuthError,            # any authentication failure
    LoginError,           # wrong credentials
    TokenRefreshError,    # /aaaRefresh.json failed
    SessionExpiredError,  # token dead, re-login also failed
    TransportError,       # any network-level error
    ConnectionError,      # host unreachable
    TimeoutError,         # request too slow
    TLSError,             # SSL/TLS certificate issue
    APIError,             # APIC returned 4xx / 5xx
    UnauthorizedError,    # 401 — token rejected by APIC
    ForbiddenError,       # 403 — insufficient privileges
    NotFoundError,        # 404 — MO does not exist
    ServerError,          # 5xx — APIC internal error
    DeserializationError, # response cannot be parsed into a typed model
    StagedPushError,      # staged design push partially succeeded
    DanglingReferenceError,  # verify_refs: external DN targets the APIC cannot honor
    SubscriptionError,             # any object-subscription (WebSocket push) failure
    StatsClassNotSubscribableError,  # subscribed to a stats class — never pushes
    SubscribeRejectedError,        # the APIC rejected a subscription=yes request
    SubscriptionLostError,         # a subscription could not be recovered
    SubscriptionLostReason,        # which recovery path was exhausted (see .reason)
)

Hierarchy:

NiwakiError
├── AuthError
│   ├── LoginError
│   ├── TokenRefreshError
│   └── SessionExpiredError
├── TransportError
│   ├── ConnectionError
│   ├── TimeoutError
│   └── TLSError
├── APIError
│   ├── UnauthorizedError
│   ├── ForbiddenError
│   ├── NotFoundError
│   └── ServerError
├── DeserializationError
├── NoResultError               (query .one() matched nothing)
├── UnknownClassError           (catalogue lookup on an unknown class; also KeyError)
├── MultipleResultsError        (query .one() matched several)
├── MissingDependencyError  (an optional extra is not installed)
├── DesignError
│   ├── UnknownMakerError          (also an AttributeError)
│   ├── DuplicateDeclarationError
│   ├── UnresolvedReferenceError
│   ├── AmbiguousBindError
│   ├── StagedPushError
│   ├── DanglingReferenceError
│   ├── SnapshotImportError        (to_design: items a snapshot holds that cannot import)
│   └── MergeConflictError         (merge: two designs disagree on one DN)
└── SubscriptionError
    ├── StatsClassNotSubscribableError
    ├── SubscribeRejectedError      (also an APIError)
    └── SubscriptionLostError       (.reason: SubscriptionLostReason)

Warnings (not exceptions — a design the fabric accepts but will fault on):

UserWarning
└── DesignHintWarning           (design/: a declaration that guarantees a fault)
exception niwaki.exceptions.APIError(status_code, apic_message='', *, apic_code=None)[source]

Bases: NiwakiError

The APIC responded with an HTTP error status (4xx or 5xx).

status_code

HTTP status code returned by the APIC — or, when the SDK raises without one, the status that best describes the failure: 404 for a DN whose read came back empty, 0 when no request was made at all. A non-zero status is therefore not proof that the controller answered; apic_code is None is the reliable way to tell an SDK-synthesised error from a controller’s own.

apic_message

Error text extracted from the APIC payload, if available; otherwise the first 200 characters of the raw body.

apic_code

The APIC’s own error code — error.attributes.code on the wire — as the verbatim string the controller sent ("103", "801"), or None when the response carried no APIC error envelope, or the SDK raised without one.

This is a cause discriminator, never a transience one: on APIC 6.0(9c) many distinct codes all arrive under HTTP 400, and none of the measured ones is retryable. Decide whether to retry from the exception type; decide what went wrong from apic_code.

The value stays a string rather than being parsed to int, so the SDK never fails to report an error because a controller sent a code it did not expect.

Example:

try:
    aci.node(dn).delete()
except APIError as exc:
    if exc.apic_code == "107":  # the controller refuses to delete it
        ...
    raise
exception niwaki.exceptions.AmbiguousBindError[source]

Bases: DesignError

No unambiguous Rs class exists for a bind edge.

Neither REFERENCE_MAP[owner][target] nor the inverse REFERENCE_MAP[target][owner] resolves to a relationship class. Use .mo(RsClass, ...) to create the relationship explicitly.

exception niwaki.exceptions.AuthError[source]

Bases: NiwakiError

Base class for APIC authentication errors.

Subclasses cover login failure, token refresh failure, and full session expiry.

exception niwaki.exceptions.ConnectionError[source]

Bases: TransportError

The APIC host is unreachable (DNS failure, TCP refused, interface down, etc.).

Wraps httpx.ConnectError in non-TLS cases.

Note

This name intentionally shadows the Python builtin ConnectionError. Inside the SDK, use niwaki.exceptions.ConnectionError.

exception niwaki.exceptions.DanglingReferenceError(failures)[source]

Bases: DesignError

External references the APIC cannot honor, caught before the push.

Raised by push(verify_refs=True) in strict/staged mode when at least one bind_dn/literal-DN reference points at a DN the APIC does not serve (or serves with a class outside the referencing class’s accept-set). Nothing has been written when this raises — verification is a read-only pass that runs before the first POST.

Every failure is collected before raising (never first-fail); the message carries the full list with DNs in clear, and failures exposes the structured checks.

Parameters:

failures (list[RefCheck]) – One RefCheck per failing reference, sorted by DN.

failures

The failing checks, as passed.

exception niwaki.exceptions.DeserializationError[source]

Bases: NiwakiError

The APIC payload cannot be parsed into a typed niwaki model.

Raised by the model layer when the response structure returned by the APIC does not match the expected Pydantic schema. This can happen if:

  • The APIC firmware version returned a field that the SDK schema does not recognise (forwards-compatibility issue).

  • A required field is missing from the APIC response (schema drift).

  • A field value has an unexpected type.

args[]

Human-readable description including the class name and the Pydantic validation error.

Example:

try:
    tenant = session.get_mo("uni/tn-Prod", fvTenant)
except DeserializationError as exc:
    logger.error("Schema mismatch for fvTenant: %s", exc)
exception niwaki.exceptions.DesignError[source]

Bases: NiwakiError

Base class for all design-DSL errors.

exception niwaki.exceptions.DesignHintWarning[source]

Bases: UserWarning

A design the SDK can express but the fabric will not be happy with.

Not an error: the push is legal, the APIC accepts it, and forbidding it would take away a shape somebody may genuinely want. It is the case where the declaration is provably going to raise a fault — a floating SVI whose address is left at 0.0.0.0 lands outside its own subnet and the controller answers with a major fault every time.

Its own category, so that warnings.simplefilter("error", DesignHintWarning) turns these into failures in a CI pipeline without touching every other UserWarning in the process, and simplefilter("ignore", DesignHintWarning) silences them without hiding anything else.

exception niwaki.exceptions.DuplicateDeclarationError[source]

Bases: DesignError

The same object (class + naming) was declared twice in a design.

exception niwaki.exceptions.ForbiddenError(status_code, apic_message='', *, apic_code=None)[source]

Bases: APIError

The APIC returned HTTP 403 — the authenticated user lacks sufficient privileges.

Difference from UnauthorizedError: - 401 = not authenticated (invalid token). - 403 = authenticated but not authorised on this resource.

exception niwaki.exceptions.LoginError[source]

Bases: AuthError

The APIC rejected the credentials during login.

Raised when POST /api/aaaLogin.json returns a non-200 status or the response contains an APIC error message (wrong password, locked account, etc.).

args[]

Error message including the HTTP status and APIC text.

exception niwaki.exceptions.MergeConflictError(conflicts)[source]

Bases: DesignError

Two designs disagree — merge() refuses to guess.

Raised after the whole merge has been walked (never first-fail): every contradiction is collected, in the style of SnapshotImportError. A contradiction is one DN carrying the same field, wire property, or class with two different values across the sources — agreement and one-sided declarations merge silently.

Parameters:

conflicts (list[tuple[str, str, tuple[object, object]]]) – One (dn, what, (value_a, value_b)) triple per contradiction, sorted by DN — what is a field name, a wire property name, or "class".

conflicts

The collected contradictions, as passed.

exception niwaki.exceptions.MissingDependencyError[source]

Bases: NiwakiError

An optional feature was used without the extra that provides it.

Raised at import or first use rather than at the first request, so the message names the extra to install instead of surfacing as a confusing failure deep in the transport.

Example:

pip install niwaki[x509]
exception niwaki.exceptions.MultipleResultsError[source]

Bases: NiwakiError

A query that required exactly one object matched more than one.

Raised by one() / one() when the result set holds two or more objects. Narrow the query, or use first() / fetch() when several matches are expected.

exception niwaki.exceptions.NiwakiError[source]

Bases: Exception

Base class for all niwaki SDK errors.

exception niwaki.exceptions.NoResultError[source]

Bases: NiwakiError

A query that required exactly one object matched none.

Raised by one() / one() when the result set is empty. Use first() when no match is an acceptable outcome.

exception niwaki.exceptions.NotFoundError(status_code, apic_message='', *, apic_code=None)[source]

Bases: APIError

The APIC returned HTTP 404 — the requested MO does not exist.

The DN or API path is invalid, or the object has been deleted.

exception niwaki.exceptions.ServerError(status_code, apic_message='', *, apic_code=None)[source]

Bases: APIError

The APIC returned a 5xx error — server-side APIC error.

These errors are considered transient and may be retried. If they persist after all retry attempts, this exception is raised.

exception niwaki.exceptions.SessionExpiredError[source]

Bases: AuthError

The session is fully expired: both refresh and re-login failed.

Raised when the token has expired and no renewal attempt succeeded. The caller must create a new ApicSession.

args[]

Message describing the reason for expiry.

exception niwaki.exceptions.SnapshotImportError(problems)[source]

Bases: DesignError

A snapshot holds items to_design() cannot import.

Raised after the whole snapshot tree has been walked (never first-fail): every offending item is collected so one run reports the complete list, in the style of DanglingReferenceError. Nothing about the failed import leaks out — the partially-built design is discarded.

The collected problems cover, by kind:

  • "unknown-class" / "unknown-property" — the shipped catalogue does not know the item (a snapshot from a newer firmware than this SDK’s schema baseline). Opt into a best-effort import with to_design(snap, on_unknown="raw"): the items are carried verbatim on the wire-attribute channel instead of raising.

  • "redacted-value" — the snapshot holds the REDACTED sentinel where a curated secret was elided at capture time; a design pushing the sentinel literally would be wrong. Opt into dropping those values with to_design(snap, redacted="skip").

  • "invalid-value" — a naming value the typed model refuses: the object’s identity cannot be built, and identity has no wire-channel escape. Non-naming values the model refuses never raise — they drop when they are the property’s schema default (an unset marker) and ride the wire channel verbatim otherwise.

  • "structure" — an RN that does not match its class’s RN format, or a repeated DN. A containment the SDK’s tables lack is not a problem: the fabric is the authority on its own edges, so the snapshot’s parent/child placement is trusted as-is.

Parameters:

problems (list[ImportProblem]) – One ImportProblem per offending item, sorted by DN.

problems

The collected problems, as passed.

exception niwaki.exceptions.StagedPushError(report, failures, not_run)[source]

Bases: DesignError

A push(mode="staged") partially succeeded.

Carries the partial PushReport (the DNs actually written, in the design’s deterministic order — not the order the controller answered in) and the failures as plain (dn, exception) pairs — no engine internals leak into the public surface.

Parameters:
  • report (PushReport) – Partial push report — report.dns are the DNs written, including every independent branch that succeeded around a failure.

  • failures (list[tuple[str, Exception]]) – (dn, exception) for every operation that failed.

  • not_run (list[str]) – DNs never attempted because an ancestor object failed — pushing them without their parent would only 404. A failure isolates its own subtree; sibling branches are still written.

Example:

from niwaki.exceptions import StagedPushError

try:
    config.push(aci, mode="staged")
except StagedPushError as exc:
    print(f"written : {exc.report.dns}")
    print(f"failed  : {[dn for dn, _ in exc.failures]}")
    print(f"skipped : {exc.not_run}")
exception niwaki.exceptions.StatsClassNotSubscribableError[source]

Bases: SubscriptionError

A subscription targeted a class the APIC can never push for.

Raised before any network I/O. Stats classes (isStat in the read catalogue) bypass the APIC’s internal event manager entirely — Cisco’s own documentation states updates are “too frequent and not scalable” to route through it — so a subscription would be silently accepted and never push anything. This is an architectural fact, unlike isObservable (see is_observable), which was empirically found not to gate subscribability and is therefore never enforced here.

exception niwaki.exceptions.SubscribeRejectedError(status_code, apic_message='', *, apic_code=None)[source]

Bases: SubscriptionError, APIError

The APIC rejected a subscription=yes request.

Multiply inherits APIError for the familiar status_code/apic_message attributes (precedent: UnknownMakerError(DesignError, AttributeError)), so a caller can inspect the HTTP status while still catching every subscription failure with a single except SubscriptionError.

exception niwaki.exceptions.SubscriptionError[source]

Bases: NiwakiError

Base class for every object-subscription failure.

exception niwaki.exceptions.SubscriptionLostError(message, *, reason=SubscriptionLostReason.RECONNECT_EXHAUSTED)[source]

Bases: SubscriptionError

A subscription could not be recovered.

Raised out of the subscription’s iterator (__next__/__anext__), terminating it. This is the one truly fatal outcome — everything short of it (a missed refresh, a reconnect that did succeed) is represented as data in the event stream instead, because there is something left to reconcile toward. See reason for which recovery path was exhausted.

reason

Which recovery path failed — see SubscriptionLostReason.

class niwaki.exceptions.SubscriptionLostReason(*values)[source]

Bases: StrEnum

Which recovery path was exhausted before SubscriptionLostError was raised.

Distinguishes the distinct fatal paths that would otherwise be indistinguishable from the error message alone:

  • RECONNECT_EXHAUSTED: the shared WebSocket itself could not be reconnected — every tracked subscription on the socket receives this.

  • RESUBSCRIBE_FAILED: the socket reconnected, but the APIC rejected this subscription’s resubscribe — sibling subscriptions on the same socket may still be fine.

  • REFRESH_ESCALATION: two consecutive scheduled refreshes failed, and the recovery resubscribe attempted for this subscription alone also failed — the socket connection itself was never affected.

  • INTERNAL_ERROR: an unexpected error escaped a background reader/refresh loop. The last-line-of-defense guard fails every tracked subscription rather than let consumers block forever on a queue nothing feeds; the socket is left reopenable by a later subscribe.

exception niwaki.exceptions.TLSError[source]

Bases: TransportError

TLS/SSL error when connecting to the APIC.

Common causes: - Self-signed certificate with verify_ssl=True (default). - Expired certificate or incorrect domain name. - Incomplete CA chain.

Quick fix (not recommended in production): verify_ssl=False.

exception niwaki.exceptions.TimeoutError[source]

Bases: TransportError

A request to the APIC exceeded the configured timeout.

Wraps httpx.TimeoutException (covers connect timeout, read timeout, write timeout, and pool timeout).

Note

This name intentionally shadows the Python builtin TimeoutError.

exception niwaki.exceptions.TokenRefreshError[source]

Bases: AuthError

Token refresh via /api/aaaRefresh.json failed.

The session will automatically attempt a full re-login as fallback. This exception should not reach the caller in the normal case; it surfaces only if the fallback itself is disabled or fails.

exception niwaki.exceptions.TransportError[source]

Bases: NiwakiError

Base class for network/transport layer errors.

All subclasses wrap corresponding httpx exceptions.

exception niwaki.exceptions.UnauthorizedError(status_code, apic_message='', *, apic_code=None)[source]

Bases: APIError

The APIC returned HTTP 401 — the session token is invalid or expired server-side.

Raised only if the 401 persists after automatic re-authentication, indicating that the credentials themselves were revoked or that the resource is not accessible to this user.

exception niwaki.exceptions.UnknownClassError[source]

Bases: NiwakiError, KeyError

A class name the read catalogue does not know.

Raised by niwaki.catalog.describe(), niwaki.catalog.class_meta(), niwaki.catalog.prop_meta() and niwaki.catalog.dn_formats() when the wire class name (or property) does not exist in the shipped catalogue — usually a typo, or a class minted by a newer APIC firmware than the one this build tracks.

Also a KeyError: callers that guarded these lookups with except KeyError before this class existed keep working unchanged — the same dual-inheritance precedent as UnknownMakerError.

exception niwaki.exceptions.UnknownMakerError[source]

Bases: DesignError, AttributeError

A maker name resolved at no level of the cursor’s ancestor path.

Also an AttributeError so that hasattr and attribute protocols keep working on cursors.

exception niwaki.exceptions.UnresolvedReferenceError[source]

Bases: DesignError

A bind/provide/consume target is not declared in the design.

Raised during closed-world validation at push time. The message includes the declared instances of the target class and a did-you-mean suggestion.