aiopynetbox
Fully async NetBox API client for Python, built on httpx2.
Inspired by pynetbox, redesigned
for asyncio. This is not a port: pynetbox's core ergonomics (lazy attribute
fetches, len() on result sets, sync generators) depend on Python protocols
that cannot be awaited, so the API surface here is deliberately different.
All I/O is explicit and awaitable, and nothing does network I/O behind your
back.
Requirements
- Python 3.11+
- NetBox 3.x / 4.x (v1 and v2
nbt_API tokens both supported)
Installation
uv add aiopynetbox # or: pip install aiopynetbox
Quick start
import asyncio
import aiopynetbox
async def main():
async with aiopynetbox.api("https://netbox.example.com", token="...") as nb:
# single object
device = await nb.dcim.devices.get(name="sw-1")
print(device.name, device.status, device.site)
# filtered query, pages are fetched concurrently
async for iface in nb.dcim.interfaces.filter(device_id=device.id):
print(iface.name)
# diff-based save: only changed fields are PATCHed
device.serial = "ABC123"
await device.save()
asyncio.run(main())
Coming from pynetbox
The traversal (nb.dcim.devices), diff-based save(), and exception taxonomy
all carry over. What changes is that implicit I/O becomes explicit:
| pynetbox (sync) | aiopynetbox (async) |
|---|---|
nb.dcim.devices.get(name="x") |
await nb.dcim.devices.get(name="x") |
for d in nb.dcim.devices.all() |
async for d in nb.dcim.devices.all() |
len(nb.dcim.devices.all()) |
await nb.dcim.devices.count() |
device.site.region (lazy fetch) |
await device.site.full_details() then device.site.region |
nb.version (property does I/O) |
await nb.version() |
threading=True |
built in: concurrent page fan-out, bounded by max_concurrency |
Nested records come back brief (as NetBox sends them). Touching a field
that isn't loaded raises AttributeError telling you to
await record.full_details(). It never fires a hidden HTTP request.
Features
- Explicit async everywhere:
httpx2.AsyncClientunder the hood, used as an async context manager so the connection pool closes deterministically. - Concurrent pagination: after the first page reveals the count, the
remaining pages are fetched in parallel (bounded by
max_concurrency, default 4) and yielded in order. - Cursor pagination (NetBox 4.6+): pass
pagination="cursor"to the client to page with thestartcursor instead, constant-time per page on very large tables (pages are sequential in this mode, since each cursor comes from the previous response). - Diff-based writes:
save()PATCHes only what you changed, with NetBox's custom_fields merge semantics handled correctly. - Bulk operations:
await nb.dcim.devices.filter(status="offline").update(comments="audit"),await recordset.delete(), and list forms on the endpoint (endpoint.update([...])/endpoint.delete([...])). - IPAM allocation:
await prefix.available_ips.create()/.list(), plusavailable_prefixesandavailable_vlans. - Plugins:
nb.plugins.<plugin>.<endpoint>andawait nb.plugins.installed_plugins(). - Choices:
await nb.dcim.devices.choices()from OPTIONS metadata. - Retries with backoff: 429 responses are retried automatically for
any method (honoring
Retry-After); transient 502/503/504 and connection failures are retried for GETs only, since an ambiguous write may already have been processed. Exponential backoff with jitter; tune withretries=, disable withretries=0. - Optimistic locking (NetBox 4.6+): records fetched from a detail
endpoint remember their
ETag;save()sendsIf-Match, so a concurrent modification fails with a 412 error instead of being silently overwritten. Repeatfull_details()calls revalidate withIf-None-Match, so unchanged objects aren't re-downloaded or re-parsed. - Data source sync:
await data_source.sync.create(). - Custom models:
aiopynetbox.register_model("plugins/bgp", "sessions", BgpSession)maps plugin endpoints to your own Record subclasses;app.endpoint("literal_name")reaches endpoint slugs that contain real underscores. - Typed: full type hints and a
py.typedmarker, plus generated hints so IDEs autocomplete endpoint names (nb.dcim.devices) and per-endpoint kwargs (filter(name=...),create(device_type=...)). Hints never restrict anything at runtime: unknown endpoints, lookup expressions, and custom-field filters keep working.
API tour
async with aiopynetbox.api(url, token=token) as nb:
# read
device = await nb.dcim.devices.get(123) # by id (None if missing)
device = await nb.dcim.devices.get(name="sw-1") # by filter (ValueError if >1)
total = await nb.dcim.devices.count(site="main")
async for d in nb.dcim.devices.filter(status="active", tag=["prod", "core"]):
...
async for d in nb.dcim.devices.all(limit=100, offset=200): # single page
...
# write
new = await nb.dcim.devices.create(name="sw-9", device_type=12, site=1, role=3)
device.serial = "XYZ"
await device.save() # PATCH {"serial": "XYZ"}
await device.update({"serial": "XYZ", "comments": "..."})
await device.delete()
# bulk
await nb.dcim.devices.filter(site="old").update(status="decommissioning")
await nb.dcim.devices.filter(status="decommissioning").delete()
# ipam allocation
prefix = await nb.ipam.prefixes.get(prefix="10.0.0.0/24")
ip = await prefix.available_ips.create() # next free IP
ips = await prefix.available_ips.create([{}, {}]) # next two
# instance info
print(await nb.version()) # "4.5"
print(await nb.status())
Long-lived apps (FastAPI, services)
Create the client once and share it; the async context manager is one-shot, so enter it for the app's lifetime, not per request:
@asynccontextmanager
async def lifespan(app: FastAPI):
async with aiopynetbox.api(url, token=token) as nb:
app.state.nb = nb
yield # handlers use `await app.state.nb...`; pool closes on shutdown
One shared instance is safe under concurrent requests. See examples/fastapi_app.py for a runnable app.
Custom httpx2 client
Pass your own httpx2.AsyncClient for custom SSL, proxies, event hooks, or
MockTransport in tests:
client = httpx2.AsyncClient(verify="/path/to/ca.pem", timeout=60)
async with aiopynetbox.api(url, token=token, client=client) as nb:
...
Per httpx2 convention, a client you pass in is yours to close: aclose()
and the context manager only close clients the Api created itself, so
one client can safely back several Api instances.
Response caching is deliberately not built in: NetBox is a source of
truth, and the library can't know your staleness tolerance. If you want
HTTP caching, pass a client using hishel's
AsyncCacheTransport and set the policy yourself.
Development
Managed with uv:
uv sync # install environment
uv run pytest # tests (in-memory fake NetBox, no network)
uv run ruff check # lint
uv run ruff format # format
License
Apache 2.0, see LICENSE.
Metadata
Release files for aiopynetbox 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| aiopynetbox-0.2.0.tar.gz | 30.3 kB | With provenance | Details for aiopynetbox-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| aiopynetbox-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for aiopynetbox-0.2.0-py3-none-any.whl |
Total release size: 63.7 kB
Release files / aiopynetbox-0.2.0.tar.gz
| Download URL | aiopynetbox-0.2.0.tar.gz |
|---|---|
| Size | 30.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
82f3e5f5505d8334c8a542aa64cf8f3fc9c79c82ddfc5d81ec96451d4d6b4a09
|
|
BLAKE2b-256 checksum How to use checksums |
50e17c99d29cf23d98dc0c892ef26e970a71d24cb883fcc376f5e4768ae2ad21
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.
Transparency logRelease files / aiopynetbox-0.2.0-py3-none-any.whl
| Download URL | aiopynetbox-0.2.0-py3-none-any.whl |
|---|---|
| Size | 33.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ee497bea67026eedd37995b435d9a5635f598d7547e40acfa07e6405eb66b7e1
|
|
BLAKE2b-256 checksum How to use checksums |
c7e296b8fc90c1ea299f2b82417359d38e2f8a7e8e1169f468ab14c77f641273
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.
Transparency logSecurity for aiopynetbox 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for aiopynetbox-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.