Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-pynetbox for openSUSE:Factory
checked in at 2026-05-07 15:44:19
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pynetbox (Old)
and /work/SRC/openSUSE:Factory/.python-pynetbox.new.1966 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pynetbox"
Thu May 7 15:44:19 2026 rev:42 rq:1351287 version:7.7.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pynetbox/python-pynetbox.changes
2026-02-04 21:10:23.823404204 +0100
+++
/work/SRC/openSUSE:Factory/.python-pynetbox.new.1966/python-pynetbox.changes
2026-05-07 15:45:36.740684584 +0200
@@ -1,0 +2,16 @@
+Wed May 6 06:23:55 UTC 2026 - Martin Hauke <[email protected]>
+
+- Update to version 7.7.0
+ New Features
+ * #762 - Support NetBox 4.6: add VirtualMachineTypes model,
+ virtual_machine_type and device typed attributes on
+ VirtualMachines, oob_ip typed attribute on Devices, and
+ dcim.cablebundle / dcim.rackgroup content type mappings
+ Bug Fixes
+ * #756 - Use v2 token auth value in create_token (NetBox 4.5+).
+ * #708 - Record.serialize() now includes new properties added
+ by the user.
+ Compatibility
+ * Supports NetBox 4.6
+
+-------------------------------------------------------------------
Old:
----
pynetbox-7.6.1.tar.gz
New:
----
pynetbox-7.7.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-pynetbox.spec ++++++
--- /var/tmp/diff_new_pack.xzMwWO/_old 2026-05-07 15:45:37.388711172 +0200
+++ /var/tmp/diff_new_pack.xzMwWO/_new 2026-05-07 15:45:37.392711337 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-pynetbox
-Version: 7.6.1
+Version: 7.7.0
Release: 0
Summary: NetBox API client library
License: Apache-2.0
++++++ pynetbox-7.6.1.tar.gz -> pynetbox-7.7.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pynetbox-7.6.1/.github/ISSUE_TEMPLATE/bug_report.yaml
new/pynetbox-7.7.0/.github/ISSUE_TEMPLATE/bug_report.yaml
--- old/pynetbox-7.6.1/.github/ISSUE_TEMPLATE/bug_report.yaml 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/.github/ISSUE_TEMPLATE/bug_report.yaml 2026-05-05
23:36:52.000000000 +0200
@@ -11,7 +11,7 @@
attributes:
label: pynetbox version
description: What version of pynetbox are you currently running?
- placeholder: v7.6.1
+ placeholder: v7.7.0
validations:
required: true
- type: input
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/.github/workflows/build-mkdocs.yml
new/pynetbox-7.7.0/.github/workflows/build-mkdocs.yml
--- old/pynetbox-7.6.1/.github/workflows/build-mkdocs.yml 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/.github/workflows/build-mkdocs.yml 2026-05-05
23:36:52.000000000 +0200
@@ -10,9 +10,13 @@
deploy:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - name: Checkout pynetbox
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
+ - name: Set up Python
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #
v6.2.0
with:
python-version: 3.x
- - run: pip install mkdocs-material mkdocs-autorefs
mkdocs-material-extensions mkdocstrings mkdocstrings-python-legacy
mkdocs-include-markdown-plugin pymdown-extensions markdown-include
- - run: mkdocs gh-deploy --force
+ - name: Install dependencies
+ run: pip install mkdocs-material mkdocs-autorefs
mkdocs-material-extensions mkdocstrings mkdocstrings-python-legacy
mkdocs-include-markdown-plugin pymdown-extensions markdown-include
+ - name: Deploy docs
+ run: mkdocs gh-deploy --force
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/.github/workflows/publish.yml
new/pynetbox-7.7.0/.github/workflows/publish.yml
--- old/pynetbox-7.6.1/.github/workflows/publish.yml 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/.github/workflows/publish.yml 2026-05-05
23:36:52.000000000 +0200
@@ -21,19 +21,20 @@
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - name: Set up Python
- uses: actions/setup-python@v3
- with:
- python-version: '3.x'
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install build
- - name: Build package
- run: python -m build
- - name: Publish package
- uses:
pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
- with:
- user: __token__
- password: ${{ secrets.PYPI_API_TOKEN }}
+ - name: Checkout pynetbox
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
+ - name: Set up Python
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #
v6.2.0
+ with:
+ python-version: '3.x'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install build
+ - name: Build package
+ run: python -m build
+ - name: Publish package
+ uses:
pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
+ with:
+ user: __token__
+ password: ${{ secrets.PYPI_API_TOKEN }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/.github/workflows/py3.yml
new/pynetbox-7.7.0/.github/workflows/py3.yml
--- old/pynetbox-7.6.1/.github/workflows/py3.yml 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/.github/workflows/py3.yml 2026-05-05
23:36:52.000000000 +0200
@@ -12,20 +12,26 @@
runs-on: ubuntu-latest
strategy:
matrix:
- python: ["3.10", "3.11", "3.12"]
- netbox: ["4.2", "4.3", "4.4"]
+ python: ["3.12", "3.13", "3.14"]
+ netbox: ["4.3", "4.4", "4.5"]
steps:
- - uses: actions/checkout@v4
+ - name: Checkout pynetbox
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #
v6.2.0
with:
python-version: ${{ matrix.python }}
- name: Install dev requirements
run: pip install -r requirements-dev.txt .
+ - name: Enable Docker IPv6
+ run: |
+ echo '{"ipv6": true, "fixed-cidr-v6": "fd00::/80"}' | sudo tee
/etc/docker/daemon.json
+ sudo systemctl reload docker
+
- name: Free up Docker resources
run: |
docker system prune -af --volumes
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/CLAUDE.md new/pynetbox-7.7.0/CLAUDE.md
--- old/pynetbox-7.6.1/CLAUDE.md 1970-01-01 01:00:00.000000000 +0100
+++ new/pynetbox-7.7.0/CLAUDE.md 2026-05-05 23:36:52.000000000 +0200
@@ -0,0 +1,189 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with
code in this repository.
+
+## Project Overview
+
+pynetbox is a Python API client library for NetBox. It provides a Pythonic
interface to interact with NetBox's REST API, supporting NetBox 3.3 and above
(pynetbox 6.7+).
+
+## Development Commands
+
+### Environment Setup
+
+```bash
+# Create and activate virtual environment
+python3 -m venv venv
+source venv/bin/activate # On Windows: venv\Scripts\activate
+
+# Install dependencies
+pip install -r requirements.txt
+pip install -r requirements-dev.txt
+```
+
+### Running Tests
+
+The test suite requires Docker to be installed and running for integration
tests.
+
+```bash
+# Run all tests
+pytest
+
+# Run only unit tests (fast, no Docker required)
+pytest tests/unit
+
+# Run only integration tests (requires Docker)
+pytest tests/integration
+
+# Run specific test file
+pytest tests/test_api.py
+
+# Run specific test function
+pytest tests/test_api.py::ApiStatusTestCase::test_api_status
+
+# Run tests matching a pattern
+pytest -k "test_api"
+
+# Run with coverage
+pytest --cov=pynetbox tests/
+```
+
+### Integration Tests - Version Control
+
+Integration tests can be run against specific NetBox versions:
+
+```bash
+# Test against specific NetBox versions (default: 4.4)
+pytest tests/integration --netbox-versions 4.4
+
+# Skip cleanup to leave Docker containers running
+pytest --no-cleanup
+
+# Use existing NetBox instance (skip Docker)
+pytest -p no:docker --url-override http://localhost:8000
+```
+
+### Linting
+
+```bash
+# Run Ruff linter
+ruff check pynetbox/ tests/
+
+# Fix auto-fixable issues
+ruff check --fix pynetbox/ tests/
+```
+
+### Building Documentation
+
+```bash
+# Build and serve docs locally
+mkdocs serve
+
+# Deploy docs to GitHub Pages
+mkdocs gh-deploy
+```
+
+## Architecture
+
+### Core Module Structure
+
+The codebase follows a layered architecture:
+
+1. **API Layer** (`pynetbox/core/api.py`):
+ - `Api` class is the main entry point
+ - Initializes NetBox app endpoints (dcim, ipam, circuits, virtualization,
extras, users, wireless, core, vpn, tenancy)
+ - Supports `plugins` via `PluginsApp` for accessing NetBox plugin APIs
+ - Manages HTTP session, authentication tokens, threading, and strict filter
validation
+
+2. **App Layer** (`pynetbox/core/app.py`):
+ - `App` class represents NetBox applications (dcim, ipam, etc.)
+ - Dynamic attribute access returns `Endpoint` objects
+ - `PluginsApp` handles plugin namespacing with dashes converted to
underscores
+
+3. **Endpoint Layer** (`pynetbox/core/endpoint.py`):
+ - `Endpoint` class provides CRUD operations for API endpoints
+ - Methods: `.all()`, `.filter()`, `.get()`, `.count()`, `.create()`,
`.update()`, `.delete()`, `.choices()`
+ - Handles parameter validation against OpenAPI spec when
`strict_filters=True`
+ - Converts underscores to dashes in endpoint names (e.g., `ip_addresses` →
`ip-addresses`)
+
+4. **Query Layer** (`pynetbox/core/query.py`):
+ - `Request` class handles HTTP communication
+ - Supports threading for `.all()` and `.filter()` operations
+ - Custom exceptions: `RequestError`, `AllocationError`, `ContentError`,
`ParameterValidationError`
+
+5. **Response Layer** (`pynetbox/core/response.py`):
+ - `Record` class represents individual API objects
+ - `RecordSet` class for collections (iterable, returned by `.all()` and
`.filter()`)
+ - Records support dict-like access, attribute access, and serialization
+ - `.save()` method for updating objects, `.delete()` for deletion
+
+### Models Module Structure
+
+Custom Record classes in `pynetbox/models/` provide specialized behavior for
specific endpoints:
+
+- `dcim.py`: DCIM-specific models (Devices, Interfaces, Cables with tracing,
etc.)
+- `ipam.py`: IP address management (IpAddresses, Prefixes, VLANs, etc.)
+- `circuits.py`: Circuit models
+- `virtualization.py`: Virtual machine models
+- `extras.py`: Custom fields, tags, webhooks
+- `users.py`: User and permission models
+- `wireless.py`: Wireless endpoint models
+- `core.py`: DataSources, Jobs, ObjectChanges models
+- `mapper.py`: `CONTENT_TYPE_MAPPER` maps NetBox content-type strings (e.g.
`"dcim.device"`) to their custom Record classes; used when resolving
polymorphic nested objects
+
+### Key Design Patterns
+
+1. **Lazy Loading**: `Endpoint` objects are created lazily via
`App.__getattr__`; top-level app objects (dcim, ipam, etc.) are created eagerly
in `Api.__init__`
+2. **Threading Support**: Enable with `threading=True` in API initialization
for parallel pagination
+3. **Custom Sessions**: Override `api.http_session` for custom SSL, timeouts,
retries
+4. **Branch Support**: Context manager `api.activate_branch()` for NetBox
branching plugin
+5. **Filter Validation**: `strict_filters=True` validates parameters against
OpenAPI spec before requests
+
+## Important Implementation Details
+
+### Serialization for API Requests
+
+When preparing objects for POST/PUT/PATCH operations, pynetbox automatically
handles:
+- Nested objects are reduced to IDs or simple values
+- Custom fields are flattened
+- Tags and certain list fields are treated as sets, not ordered lists
+- Use `.serialize()` method to see how an object will be sent to the API
+
+### Cable Terminations and Complex Objects
+
+Some NetBox objects like Cables have complex nested structures. The `dcim.py`
models contain specialized handling:
+- `TraceableRecord`: Base class for objects that support cable tracing
+- `Cables`: Handles A/B terminations with proper serialization
+- DetailEndpoints vs regular Endpoints for objects without list views
+
+### Test Structure
+
+- `tests/test_*.py`: Older-style unit tests (circuits, tenancy, users,
virtualization, wireless, api, app)
+- `tests/unit/`: Newer unit tests that mock HTTP responses
+- `tests/integration/`: Integration tests against real NetBox instances in
Docker
+- `tests/conftest.py`: Shared pytest configuration and fixtures (defines
`--netbox-versions`, `--no-cleanup`, `--url-override` options)
+- `tests/integration/conftest.py`: Docker setup for spinning up NetBox
containers
+
+The integration test framework uses `pytest-docker` to:
+1. Pull and launch netbox-docker containers
+2. Wait for NetBox to be ready
+3. Run tests against the live instance
+4. Clean up containers after tests (unless `--no-cleanup` specified)
+
+## Branching and Git Workflow
+
+- Main branch: `master`
+- Current working branch visible in git status
+- Commit messages should prefix with "Fixes" or "Closes" and issue number
(e.g., "Closes #1234: Add IPv5 support")
+- Pull requests require approval for accepted issues only (trivial
documentation changes excepted)
+
+## Version Compatibility
+
+Current version: 7.6.1
+
+NetBox version compatibility is strict:
+- pynetbox 7.6.1 supports NetBox 4.5.0
+- pynetbox 7.5.0 supports NetBox 4.1, 4.2, 4.3
+- pynetbox 7.4.1 supports NetBox 4.0.6
+- pynetbox 7.0.0+ requires NetBox 3.3+
+
+When working on features, verify compatibility with the supported NetBox
versions via integration tests.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/PKG-INFO new/pynetbox-7.7.0/PKG-INFO
--- old/pynetbox-7.6.1/PKG-INFO 2026-01-28 17:50:43.822330500 +0100
+++ new/pynetbox-7.7.0/PKG-INFO 2026-05-05 23:37:04.070566000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: pynetbox
-Version: 7.6.1
+Version: 7.7.0
Summary: NetBox API client library
Home-page: https://github.com/netbox-community/pynetbox
Author: Zach Moody, Arthur Hanson
@@ -40,6 +40,7 @@
| NetBox Version | Plugin Version |
|:--------------:|:--------------:|
+| 4.6 | 7.7.0 |
| 4.5 | 7.6.1 |
| 4.5 | 7.6.0 |
| 4.4 | 7.5.0 |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/README.md new/pynetbox-7.7.0/README.md
--- old/pynetbox-7.6.1/README.md 2026-01-28 17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/README.md 2026-05-05 23:36:52.000000000 +0200
@@ -9,6 +9,7 @@
| NetBox Version | Plugin Version |
|:--------------:|:--------------:|
+| 4.6 | 7.7.0 |
| 4.5 | 7.6.1 |
| 4.5 | 7.6.0 |
| 4.4 | 7.5.0 |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/docs/IPAM.md
new/pynetbox-7.7.0/docs/IPAM.md
--- old/pynetbox-7.6.1/docs/IPAM.md 2026-01-28 17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/docs/IPAM.md 1970-01-01 01:00:00.000000000 +0100
@@ -1,163 +0,0 @@
-# IPAM
-
-This page documents special methods available for IPAM models in pyNetBox.
-
-!!! note "Standard API Operations"
- Standard CRUD operations (`.all()`, `.filter()`, `.get()`, `.create()`,
`.update()`, `.delete()`) follow NetBox's REST API patterns. Refer to the
[NetBox API documentation](https://demo.netbox.dev/api/docs/) for details on
available endpoints and filters.
-
-## Prefixes
-
-### Available IPs
-
-The `available_ips` property provides access to view and create available IP
addresses within a prefix.
-
-::: pynetbox.models.ipam.Prefixes.available_ips
- handler: python
- options:
- show_source: true
-
-**Examples:**
-```python
-prefix = nb.ipam.prefixes.get(prefix='10.0.0.0/24')
-
-# List available IP addresses
-available = prefix.available_ips.list()
-# [10.0.0.1/24, 10.0.0.2/24, 10.0.0.3/24, ...]
-
-# Create a single IP from available pool
-new_ip = prefix.available_ips.create()
-
-# Create multiple IPs
-new_ips = prefix.available_ips.create([{} for i in range(5)])
-
-# Create IP with specific attributes
-new_ip = prefix.available_ips.create({
- 'dns_name': 'server01.example.com',
- 'description': 'Web Server',
- 'status': 'active'
-})
-```
-
-### Available Prefixes
-
-The `available_prefixes` property provides access to view and create available
child prefixes within a parent prefix.
-
-::: pynetbox.models.ipam.Prefixes.available_prefixes
- handler: python
- options:
- show_source: true
-
-**Examples:**
-```python
-prefix = nb.ipam.prefixes.get(prefix='10.0.0.0/16')
-
-# List available child prefixes
-available = prefix.available_prefixes.list()
-# [10.0.1.0/24, 10.0.2.0/23, 10.0.4.0/22, ...]
-
-# Create a child prefix
-new_prefix = prefix.available_prefixes.create({
- 'prefix_length': 24,
- 'status': 'active',
- 'description': 'Server subnet'
-})
-
-# Create multiple child prefixes
-new_prefixes = prefix.available_prefixes.create([
- {'prefix_length': 24},
- {'prefix_length': 24},
- {'prefix_length': 25}
-])
-```
-
-## IP Ranges
-
-### Available IPs
-
-The `available_ips` property provides access to view and create available IP
addresses within an IP range.
-
-::: pynetbox.models.ipam.IpRanges.available_ips
- handler: python
- options:
- show_source: true
-
-**Examples:**
-```python
-ip_range = nb.ipam.ip_ranges.get(1)
-
-# List available IPs in range
-available = ip_range.available_ips.list()
-
-# Create single IP from range
-new_ip = ip_range.available_ips.create()
-
-# Create multiple IPs
-new_ips = ip_range.available_ips.create([{} for i in range(10)])
-
-# Create IP with attributes
-new_ip = ip_range.available_ips.create({
- 'description': 'DHCP reservation',
- 'status': 'reserved'
-})
-```
-
-## VLAN Groups
-
-### Available VLANs
-
-The `available_vlans` property provides access to view and create available
VLANs within a VLAN group.
-
-::: pynetbox.models.ipam.VlanGroups.available_vlans
- handler: python
- options:
- show_source: true
-
-**Examples:**
-```python
-vlan_group = nb.ipam.vlan_groups.get(name='Production')
-
-# List available VLAN IDs
-available = vlan_group.available_vlans.list()
-# [10, 11, 12, 13, ...]
-
-# Create a VLAN from available IDs
-new_vlan = vlan_group.available_vlans.create({
- 'name': 'NewVLAN',
- 'status': 'active'
-})
-# NewVLAN (VID: 10)
-
-# Create VLAN with specific VID (must be in available range)
-new_vlan = vlan_group.available_vlans.create({
- 'name': 'Servers',
- 'vid': 100,
- 'status': 'active'
-})
-```
-
-## ASN Ranges
-
-### Available ASNs
-
-The `available_asns` property provides access to view and create available
ASNs within an ASN range.
-
-::: pynetbox.models.ipam.AsnRanges.available_asns
- handler: python
- options:
- show_source: true
-
-**Examples:**
-```python
-asn_range = nb.ipam.asn_ranges.get(name='Private ASN Pool')
-
-# List available ASNs
-available = asn_range.available_asns.list()
-# [64512, 64513, 64514, ...]
-
-# Allocate a single ASN
-new_asn = asn_range.available_asns.create()
-# 64512
-
-# Allocate multiple ASNs
-new_asns = asn_range.available_asns.create([{} for i in range(5)])
-```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/docs/ipam.md
new/pynetbox-7.7.0/docs/ipam.md
--- old/pynetbox-7.6.1/docs/ipam.md 1970-01-01 01:00:00.000000000 +0100
+++ new/pynetbox-7.7.0/docs/ipam.md 2026-05-05 23:36:52.000000000 +0200
@@ -0,0 +1,163 @@
+# IPAM
+
+This page documents special methods available for IPAM models in pyNetBox.
+
+!!! note "Standard API Operations"
+ Standard CRUD operations (`.all()`, `.filter()`, `.get()`, `.create()`,
`.update()`, `.delete()`) follow NetBox's REST API patterns. Refer to the
[NetBox API documentation](https://demo.netbox.dev/api/docs/) for details on
available endpoints and filters.
+
+## Prefixes
+
+### Available IPs
+
+The `available_ips` property provides access to view and create available IP
addresses within a prefix.
+
+::: pynetbox.models.ipam.Prefixes.available_ips
+ handler: python
+ options:
+ show_source: true
+
+**Examples:**
+```python
+prefix = nb.ipam.prefixes.get(prefix='10.0.0.0/24')
+
+# List available IP addresses
+available = prefix.available_ips.list()
+# [10.0.0.1/24, 10.0.0.2/24, 10.0.0.3/24, ...]
+
+# Create a single IP from available pool
+new_ip = prefix.available_ips.create()
+
+# Create multiple IPs
+new_ips = prefix.available_ips.create([{} for i in range(5)])
+
+# Create IP with specific attributes
+new_ip = prefix.available_ips.create({
+ 'dns_name': 'server01.example.com',
+ 'description': 'Web Server',
+ 'status': 'active'
+})
+```
+
+### Available Prefixes
+
+The `available_prefixes` property provides access to view and create available
child prefixes within a parent prefix.
+
+::: pynetbox.models.ipam.Prefixes.available_prefixes
+ handler: python
+ options:
+ show_source: true
+
+**Examples:**
+```python
+prefix = nb.ipam.prefixes.get(prefix='10.0.0.0/16')
+
+# List available child prefixes
+available = prefix.available_prefixes.list()
+# [10.0.1.0/24, 10.0.2.0/23, 10.0.4.0/22, ...]
+
+# Create a child prefix
+new_prefix = prefix.available_prefixes.create({
+ 'prefix_length': 24,
+ 'status': 'active',
+ 'description': 'Server subnet'
+})
+
+# Create multiple child prefixes
+new_prefixes = prefix.available_prefixes.create([
+ {'prefix_length': 24},
+ {'prefix_length': 24},
+ {'prefix_length': 25}
+])
+```
+
+## IP Ranges
+
+### Available IPs
+
+The `available_ips` property provides access to view and create available IP
addresses within an IP range.
+
+::: pynetbox.models.ipam.IpRanges.available_ips
+ handler: python
+ options:
+ show_source: true
+
+**Examples:**
+```python
+ip_range = nb.ipam.ip_ranges.get(1)
+
+# List available IPs in range
+available = ip_range.available_ips.list()
+
+# Create single IP from range
+new_ip = ip_range.available_ips.create()
+
+# Create multiple IPs
+new_ips = ip_range.available_ips.create([{} for i in range(10)])
+
+# Create IP with attributes
+new_ip = ip_range.available_ips.create({
+ 'description': 'DHCP reservation',
+ 'status': 'reserved'
+})
+```
+
+## VLAN Groups
+
+### Available VLANs
+
+The `available_vlans` property provides access to view and create available
VLANs within a VLAN group.
+
+::: pynetbox.models.ipam.VlanGroups.available_vlans
+ handler: python
+ options:
+ show_source: true
+
+**Examples:**
+```python
+vlan_group = nb.ipam.vlan_groups.get(name='Production')
+
+# List available VLAN IDs
+available = vlan_group.available_vlans.list()
+# [10, 11, 12, 13, ...]
+
+# Create a VLAN from available IDs
+new_vlan = vlan_group.available_vlans.create({
+ 'name': 'NewVLAN',
+ 'status': 'active'
+})
+# NewVLAN (VID: 10)
+
+# Create VLAN with specific VID (must be in available range)
+new_vlan = vlan_group.available_vlans.create({
+ 'name': 'Servers',
+ 'vid': 100,
+ 'status': 'active'
+})
+```
+
+## ASN Ranges
+
+### Available ASNs
+
+The `available_asns` property provides access to view and create available
ASNs within an ASN range.
+
+::: pynetbox.models.ipam.AsnRanges.available_asns
+ handler: python
+ options:
+ show_source: true
+
+**Examples:**
+```python
+asn_range = nb.ipam.asn_ranges.get(name='Private ASN Pool')
+
+# List available ASNs
+available = asn_range.available_asns.list()
+# [64512, 64513, 64514, ...]
+
+# Allocate a single ASN
+new_asn = asn_range.available_asns.create()
+# 64512
+
+# Allocate multiple ASNs
+new_asns = asn_range.available_asns.create([{} for i in range(5)])
+```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/docs/release-notes.md
new/pynetbox-7.7.0/docs/release-notes.md
--- old/pynetbox-7.6.1/docs/release-notes.md 2026-01-28 17:50:36.000000000
+0100
+++ new/pynetbox-7.7.0/docs/release-notes.md 2026-05-05 23:36:52.000000000
+0200
@@ -1,5 +1,19 @@
# Release Notes
+## Version 7.7.0 (May 5, 2026)
+
+#### New Features
+- [#762](https://github.com/netbox-community/pynetbox/issues/762) - Support
NetBox 4.6: add `VirtualMachineTypes` model, `virtual_machine_type` and
`device` typed attributes on `VirtualMachines`, `oob_ip` typed attribute on
`Devices`, and `dcim.cablebundle` / `dcim.rackgroup` content type mappings
+
+#### Bug Fixes
+- [#756](https://github.com/netbox-community/pynetbox/issues/756) - Use v2
token auth value in `create_token` (NetBox 4.5+)
+- [#708](https://github.com/netbox-community/pynetbox/issues/708) -
`Record.serialize()` now includes new properties added by the user
+
+#### Compatibility
+- Supports NetBox 4.6
+
+---
+
## Version 7.6.1 (January 28, 2026)
#### Enhancements
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/pynetbox/__init__.py
new/pynetbox-7.7.0/pynetbox/__init__.py
--- old/pynetbox-7.6.1/pynetbox/__init__.py 2026-01-28 17:50:36.000000000
+0100
+++ new/pynetbox-7.7.0/pynetbox/__init__.py 2026-05-05 23:36:52.000000000
+0200
@@ -6,7 +6,7 @@
ParameterValidationError,
)
-__version__ = "7.6.1"
+__version__ = "7.7.0"
# Lowercase alias for backward compatibility
api = Api
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/pynetbox/core/api.py
new/pynetbox-7.7.0/pynetbox/core/api.py
--- old/pynetbox-7.6.1/pynetbox/core/api.py 2026-01-28 17:50:36.000000000
+0100
+++ new/pynetbox-7.7.0/pynetbox/core/api.py 2026-05-05 23:36:52.000000000
+0200
@@ -19,7 +19,7 @@
import requests
from pynetbox.core.app import App, PluginsApp
-from pynetbox.core.query import Request
+from pynetbox.core.query import Request, TOKEN_PREFIX
from pynetbox.core.response import Record
@@ -224,42 +224,40 @@
## Raises
`RequestError`: If the request is not successful.
+ ## Notes
+
+ NetBox 4.5 introduced v2 tokens. For v2 tokens, `nb.token` is set to
+ `nbt_<key>.<token>` (the full auth value required in the Authorization
+ header), which differs from `token.key`. For v1 tokens (pre-4.5),
+ `nb.token` is the plaintext token value.
+
## Example
```python
import pynetbox
nb = pynetbox.api("https://netbox-server")
token = nb.create_token("admin", "netboxpassword")
+
+ # NetBox 4.5+ v2 token: nb.token differs from token.key
nb.token
- # '96d02e13e3f1fdcd8b4c089094c0191dcb045bef'
+ # 'nbt_shortkey1234567.plaintexttoken7890abcdef1234567890abcdef'
+ token.key
+ # 'shortkey1234567'
- from pprint import pprint
- pprint(dict(token))
- {
- 'created': '2021-11-27T11:26:49.360185+02:00',
- 'description': '',
- 'display': '045bef (admin)',
- 'expires': None,
- 'id': 2,
- 'key': '96d02e13e3f1fdcd8b4c089094c0191dcb045bef',
- 'url': 'https://netbox-server/api/users/tokens/2/',
- 'user': {
- 'display': 'admin',
- 'id': 1,
- 'url': 'https://netbox-server/api/users/users/1/',
- 'username': 'admin'
- },
- 'write_enabled': True
- }
+ # Pre-4.5 / v1 token: nb.token matches token.key (or token.token)
+ nb.token
+ # '96d02e13e3f1fdcd8b4c089094c0191dcb045bef'
```
"""
resp = Request(
base="{}/users/tokens/provision/".format(self.base_url),
http_session=self.http_session,
).post(data={"username": username, "password": password})
- # Save the newly created API token, otherwise populating the Record
- # object details will fail
- self.token = resp["key"]
+ # v2 tokens (NetBox 4.5+): construct auth value as nbt_<key>.<token>
+ if resp.get("version") == 2:
+ self.token = "{}{}.{}".format(TOKEN_PREFIX, resp["key"],
resp["token"])
+ else:
+ self.token = resp.get("token") or resp["key"]
return Record(resp, self, None)
@contextlib.contextmanager
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/pynetbox/core/response.py
new/pynetbox-7.7.0/pynetbox/core/response.py
--- old/pynetbox-7.6.1/pynetbox/core/response.py 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/pynetbox/core/response.py 2026-05-05
23:36:52.000000000 +0200
@@ -295,6 +295,18 @@
url = None
+ # Internal Record metadata that should not be serialized for API updates.
+ # These are object bookkeeping attributes, not NetBox API fields.
+ _INTERNAL_ATTRS = frozenset(
+ [
+ "api", # API client instance
+ "endpoint", # Endpoint object reference
+ "url", # Object URL (read-only field provided by API)
+ "has_details", # Flag for lazy-loading full details
+ "default_ret", # Default Record class for nested objects
+ ]
+ )
+
def __init__(self, values, api, endpoint):
self.has_details = False
self._full_cache = []
@@ -538,6 +550,14 @@
If an attribute's value is a ``Record`` type it's replaced with
the ``id`` field of that object.
+ When ``init=False`` (default), includes both original fields from the
+ API response and any fields that have been set on the object after
+ initialization. This allows proper change detection for fields set
+ to None or other values.
+
+ When ``init=True``, returns only the original fields from the initial
+ API response, used for comparing against the current state to detect
+ changes.
.. note::
@@ -550,13 +570,31 @@
if nested:
return get_return(self)
+ # Determine which fields to serialize
if init:
- init_vals = dict(self._init_cache)
+ # For initial state, use only _init_cache
+ init_cache_dict = dict(self._init_cache)
+ fields_to_serialize = init_cache_dict.keys()
+ init_vals = init_cache_dict
+ else:
+ # For current state, include all fields (original + modified)
+ init_cache_keys = {k for k, _ in self._init_cache}
+
+ # Get all non-internal field names from object's __dict__
+ obj_keys = {
+ k
+ for k in self.__dict__.keys()
+ if not k.startswith("_") and k not in self._INTERNAL_ATTRS
+ }
+
+ # Combine both sets
+ fields_to_serialize = init_cache_keys | obj_keys
+ init_vals = {} # Not used when init=False
ret = {}
- for i in dict(self):
- current_val = getattr(self, i) if not init else init_vals.get(i)
+ for i in fields_to_serialize:
+ current_val = getattr(self, i, None) if not init else
init_vals.get(i)
if i == "custom_fields":
ret[i] = flatten_custom(current_val)
else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/pynetbox/models/dcim.py
new/pynetbox-7.7.0/pynetbox/models/dcim.py
--- old/pynetbox-7.6.1/pynetbox/models/dcim.py 2026-01-28 17:50:36.000000000
+0100
+++ new/pynetbox-7.7.0/pynetbox/models/dcim.py 2026-05-05 23:36:52.000000000
+0200
@@ -84,6 +84,7 @@
primary_ip = IpAddresses
primary_ip4 = IpAddresses
primary_ip6 = IpAddresses
+ oob_ip = IpAddresses
local_context_data = JsonField
config_context = JsonField
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/pynetbox/models/mapper.py
new/pynetbox-7.7.0/pynetbox/models/mapper.py
--- old/pynetbox-7.6.1/pynetbox/models/mapper.py 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/pynetbox/models/mapper.py 2026-05-05
23:36:52.000000000 +0200
@@ -23,7 +23,7 @@
VirtualChassis,
)
from .ipam import Aggregates, IpAddresses, Prefixes, VlanGroups, Vlans
-from .virtualization import VirtualMachines
+from .virtualization import VirtualMachineTypes, VirtualMachines
from .wireless import WirelessLans
CONTENT_TYPE_MAPPER = {
@@ -35,6 +35,7 @@
"core.job": Jobs,
"core.objectchange": ObjectChanges,
"dcim.cable": Cables,
+ "dcim.cablebundle": None,
"dcim.cablepath": None,
"dcim.cabletermination": Termination,
"dcim.consoleport": ConsolePorts,
@@ -67,6 +68,7 @@
"dcim.powerport": PowerPorts,
"dcim.powerporttemplate": None,
"dcim.rack": Racks,
+ "dcim.rackgroup": None,
"dcim.rackreservation": RackReservations,
"dcim.rackrole": None,
"dcim.rearport": RearPorts,
@@ -116,6 +118,7 @@
"virtualization.clustertype": None,
"virtualization.interface": None,
"virtualization.virtualmachine": VirtualMachines,
+ "virtualization.virtualmachinetype": VirtualMachineTypes,
"wireless.WirelessLAN": WirelessLans,
"wireless.WirelessLANGroup": None,
"wireless.wirelesslink": None,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/pynetbox/models/virtualization.py
new/pynetbox-7.7.0/pynetbox/models/virtualization.py
--- old/pynetbox-7.6.1/pynetbox/models/virtualization.py 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/pynetbox/models/virtualization.py 2026-05-05
23:36:52.000000000 +0200
@@ -16,14 +16,21 @@
from pynetbox.core.endpoint import DetailEndpoint
from pynetbox.core.response import JsonField, Record
+from pynetbox.models.dcim import Devices
from pynetbox.models.ipam import IpAddresses
+class VirtualMachineTypes(Record):
+ pass
+
+
class VirtualMachines(Record):
+ device = Devices
primary_ip = IpAddresses
primary_ip4 = IpAddresses
primary_ip6 = IpAddresses
config_context = JsonField
+ virtual_machine_type = VirtualMachineTypes
@property
def render_config(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/pynetbox.egg-info/PKG-INFO
new/pynetbox-7.7.0/pynetbox.egg-info/PKG-INFO
--- old/pynetbox-7.6.1/pynetbox.egg-info/PKG-INFO 2026-01-28
17:50:43.000000000 +0100
+++ new/pynetbox-7.7.0/pynetbox.egg-info/PKG-INFO 2026-05-05
23:37:03.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: pynetbox
-Version: 7.6.1
+Version: 7.7.0
Summary: NetBox API client library
Home-page: https://github.com/netbox-community/pynetbox
Author: Zach Moody, Arthur Hanson
@@ -40,6 +40,7 @@
| NetBox Version | Plugin Version |
|:--------------:|:--------------:|
+| 4.6 | 7.7.0 |
| 4.5 | 7.6.1 |
| 4.5 | 7.6.0 |
| 4.4 | 7.5.0 |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/pynetbox.egg-info/SOURCES.txt
new/pynetbox-7.7.0/pynetbox.egg-info/SOURCES.txt
--- old/pynetbox-7.6.1/pynetbox.egg-info/SOURCES.txt 2026-01-28
17:50:43.000000000 +0100
+++ new/pynetbox-7.7.0/pynetbox.egg-info/SOURCES.txt 2026-05-05
23:37:04.000000000 +0200
@@ -2,6 +2,7 @@
.pre-commit-config.yaml
.readthedocs.yaml
CHANGELOG.md
+CLAUDE.md
LICENSE
README.md
mkdocs.yml
@@ -18,7 +19,6 @@
.github/workflows/build-mkdocs.yml
.github/workflows/publish.yml
.github/workflows/py3.yml
-docs/IPAM.md
docs/advanced.md
docs/api.md
docs/branching.md
@@ -28,6 +28,7 @@
docs/getting-started.md
docs/index.md
docs/installation.md
+docs/ipam.md
docs/release-notes.md
docs/request.md
docs/response.md
@@ -65,12 +66,15 @@
tests/test_api.py
tests/test_app.py
tests/test_circuits.py
+tests/test_dcim.py
tests/test_tenancy.py
tests/test_users.py
tests/test_virtualization.py
tests/test_wireless.py
tests/util.py
-tests/fixtures/api/token_provision.json
+tests/fixtures/api/token_provision_v1_legacy.json
+tests/fixtures/api/token_provision_v1_with_token.json
+tests/fixtures/api/token_provision_v2.json
tests/fixtures/circuits/circuit.json
tests/fixtures/circuits/circuit_termination.json
tests/fixtures/circuits/circuit_terminations.json
@@ -180,6 +184,8 @@
tests/fixtures/virtualization/interface.json
tests/fixtures/virtualization/interfaces.json
tests/fixtures/virtualization/virtual_machine.json
+tests/fixtures/virtualization/virtual_machine_type.json
+tests/fixtures/virtualization/virtual_machine_types.json
tests/fixtures/virtualization/virtual_machines.json
tests/fixtures/wireless/wireless_lan.json
tests/fixtures/wireless/wireless_lans.json
@@ -193,6 +199,7 @@
tests/unit/test_endpoint_strict_filter.py
tests/unit/test_extras.py
tests/unit/test_file_upload.py
+tests/unit/test_mapper.py
tests/unit/test_multiformat_endpoint.py
tests/unit/test_query.py
tests/unit/test_request.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pynetbox-7.6.1/tests/fixtures/api/token_provision.json
new/pynetbox-7.7.0/tests/fixtures/api/token_provision.json
--- old/pynetbox-7.6.1/tests/fixtures/api/token_provision.json 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/tests/fixtures/api/token_provision.json 1970-01-01
01:00:00.000000000 +0100
@@ -1,3 +0,0 @@
-{
- "key": "1234567890123456789012345678901234567890"
-}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pynetbox-7.6.1/tests/fixtures/api/token_provision_v1_legacy.json
new/pynetbox-7.7.0/tests/fixtures/api/token_provision_v1_legacy.json
--- old/pynetbox-7.6.1/tests/fixtures/api/token_provision_v1_legacy.json
1970-01-01 01:00:00.000000000 +0100
+++ new/pynetbox-7.7.0/tests/fixtures/api/token_provision_v1_legacy.json
2026-05-05 23:36:52.000000000 +0200
@@ -0,0 +1,3 @@
+{
+ "key": "1234567890123456789012345678901234567890"
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pynetbox-7.6.1/tests/fixtures/api/token_provision_v1_with_token.json
new/pynetbox-7.7.0/tests/fixtures/api/token_provision_v1_with_token.json
--- old/pynetbox-7.6.1/tests/fixtures/api/token_provision_v1_with_token.json
1970-01-01 01:00:00.000000000 +0100
+++ new/pynetbox-7.7.0/tests/fixtures/api/token_provision_v1_with_token.json
2026-05-05 23:36:52.000000000 +0200
@@ -0,0 +1,6 @@
+{
+ "id": 1,
+ "version": 1,
+ "key": "1234567890123456789012345678901234567890",
+ "token": "plaintexttoken7890abcdef1234567890abcdef"
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pynetbox-7.6.1/tests/fixtures/api/token_provision_v2.json
new/pynetbox-7.7.0/tests/fixtures/api/token_provision_v2.json
--- old/pynetbox-7.6.1/tests/fixtures/api/token_provision_v2.json
1970-01-01 01:00:00.000000000 +0100
+++ new/pynetbox-7.7.0/tests/fixtures/api/token_provision_v2.json
2026-05-05 23:36:52.000000000 +0200
@@ -0,0 +1,6 @@
+{
+ "id": 1,
+ "version": 2,
+ "key": "shortkey1234567",
+ "token": "plaintexttoken7890abcdef1234567890abcdef"
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/tests/fixtures/dcim/device.json
new/pynetbox-7.7.0/tests/fixtures/dcim/device.json
--- old/pynetbox-7.6.1/tests/fixtures/dcim/device.json 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/tests/fixtures/dcim/device.json 2026-05-05
23:36:52.000000000 +0200
@@ -64,6 +64,12 @@
"address": "10.0.255.1/32"
},
"primary_ip6": null,
+ "oob_ip": {
+ "id": 5,
+ "url": "http://localhost:8000/api/ipam/ip-addresses/5/",
+ "family": 4,
+ "address": "192.0.2.1/32"
+ },
"comments": "",
"local_context_data": {
"testing": "test"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pynetbox-7.6.1/tests/fixtures/virtualization/virtual_machine.json
new/pynetbox-7.7.0/tests/fixtures/virtualization/virtual_machine.json
--- old/pynetbox-7.6.1/tests/fixtures/virtualization/virtual_machine.json
2026-01-28 17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/tests/fixtures/virtualization/virtual_machine.json
2026-05-05 23:36:52.000000000 +0200
@@ -5,10 +5,17 @@
"value": 1,
"label": "Active"
},
- "cluster": {
+ "cluster": null,
+ "device": {
"id": 1,
- "url": "http://localhost:8000/api/virtualization/clusters/1/",
- "name": "vm-test-cluster"
+ "url": "http://localhost:8000/api/dcim/devices/1/",
+ "name": "test-device"
+ },
+ "virtual_machine_type": {
+ "id": 1,
+ "url":
"http://localhost:8000/api/virtualization/virtual-machine-types/1/",
+ "name": "Standard",
+ "slug": "standard"
},
"role": null,
"tenant": null,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pynetbox-7.6.1/tests/fixtures/virtualization/virtual_machine_type.json
new/pynetbox-7.7.0/tests/fixtures/virtualization/virtual_machine_type.json
--- old/pynetbox-7.6.1/tests/fixtures/virtualization/virtual_machine_type.json
1970-01-01 01:00:00.000000000 +0100
+++ new/pynetbox-7.7.0/tests/fixtures/virtualization/virtual_machine_type.json
2026-05-05 23:36:52.000000000 +0200
@@ -0,0 +1,6 @@
+{
+ "id": 1,
+ "url": "http://localhost:8000/api/virtualization/virtual-machine-types/1/",
+ "name": "Standard",
+ "slug": "standard"
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pynetbox-7.6.1/tests/fixtures/virtualization/virtual_machine_types.json
new/pynetbox-7.7.0/tests/fixtures/virtualization/virtual_machine_types.json
--- old/pynetbox-7.6.1/tests/fixtures/virtualization/virtual_machine_types.json
1970-01-01 01:00:00.000000000 +0100
+++ new/pynetbox-7.7.0/tests/fixtures/virtualization/virtual_machine_types.json
2026-05-05 23:36:52.000000000 +0200
@@ -0,0 +1,13 @@
+{
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "url":
"http://localhost:8000/api/virtualization/virtual-machine-types/1/",
+ "name": "Standard",
+ "slug": "standard"
+ }
+ ]
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/tests/integration/conftest.py
new/pynetbox-7.7.0/tests/integration/conftest.py
--- old/pynetbox-7.6.1/tests/integration/conftest.py 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/tests/integration/conftest.py 2026-05-05
23:36:52.000000000 +0200
@@ -32,6 +32,8 @@
tag = "3.3.0"
elif (major, minor) == (4, 4):
tag = "3.4.2"
+ elif (major, minor) == (4, 5):
+ tag = "4.0.2"
else:
raise NotImplementedError(
"Version %s is not currently supported" % netbox_version
@@ -263,14 +265,18 @@
# ensure the netbox container listens on a random port
new_services[new_service_name]["ports"] = ["8080"]
- # Increase health check timeouts for GitHub Actions runners
- # which may have more resource constraints
+ # Increase health check timeouts for GitHub Actions
runners.
+ # Granian (netbox-docker 4.x) binds to "::" (IPv6); try
+ # explicit IPv4 loopback first via dual-stack, then IPv6.
new_services[new_service_name]["healthcheck"] = {
- "test": "curl -f http://localhost:8080/login/ || exit
1",
- "start_period": "180s", # Increased from 90s
- "timeout": "10s", # Increased from 3s
+ "test": [
+ "CMD-SHELL",
+ "curl -sf http://127.0.0.1:8080/login/ || curl -sf
http://[::1]:8080/login/",
+ ],
+ "start_period": "360s",
+ "timeout": "10s",
"interval": "15s",
- "retries": 5,
+ "retries": 10,
}
# set the network and an alias to the proper short name of the
container
@@ -281,20 +287,18 @@
# fix the naming of any dependencies
if "depends_on" in new_services[new_service_name]:
- new_service_dependencies = []
- for dependent_service_name in
new_services[new_service_name][
- "depends_on"
- ]:
- new_service_dependencies.append(
- "netbox_v%s_%s"
- % (
- docker_netbox_version,
- dependent_service_name,
- )
- )
- new_services[new_service_name][
- "depends_on"
- ] = new_service_dependencies
+ depends_on = new_services[new_service_name]["depends_on"]
+ if isinstance(depends_on, dict):
+ # Dict form: {service: {condition: service_healthy}} —
preserve conditions
+ new_services[new_service_name]["depends_on"] = {
+ "netbox_v%s_%s" % (docker_netbox_version, dep): cfg
+ for dep, cfg in depends_on.items()
+ }
+ else:
+ new_services[new_service_name]["depends_on"] = [
+ "netbox_v%s_%s" % (docker_netbox_version, dep)
+ for dep in depends_on
+ ]
# make any internal named volumes unique to the netbox version
if "volumes" in new_services[new_service_name]:
@@ -449,9 +453,9 @@
@pytest.fixture(scope="session")
def api(docker_netbox_service):
- return pynetbox.api(
- docker_netbox_service["url"],
token="0123456789abcdef0123456789abcdef01234567"
- )
+ nb = pynetbox.api(docker_netbox_service["url"])
+ nb.create_token("admin", "admin")
+ return nb
@pytest.fixture(scope="session")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/tests/integration/test_dcim.py
new/pynetbox-7.7.0/tests/integration/test_dcim.py
--- old/pynetbox-7.6.1/tests/integration/test_dcim.py 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/tests/integration/test_dcim.py 2026-05-05
23:36:52.000000000 +0200
@@ -97,11 +97,8 @@
i.delete()
def test_threading_duplicates(self, docker_netbox_service, add_sites):
- api = pynetbox.api(
- docker_netbox_service["url"],
- token="0123456789abcdef0123456789abcdef01234567",
- threading=True,
- )
+ api = pynetbox.api(docker_netbox_service["url"], threading=True)
+ api.create_token("admin", "admin")
test = api.dcim.sites.all(limit=5)
test_list = list(test)
test_set = set(test_list)
@@ -330,14 +327,14 @@
device.delete()
@pytest.fixture(scope="class")
- def front_port_a(self, api, device_a, rear_port_a):
- ret = api.dcim.front_ports.create(
- name="FrontPort1",
- device=device_a.id,
- type="8p8c",
- rear_port=rear_port_a.id,
- rear_port_position=1,
- )
+ def front_port_a(self, api, device_a, rear_port_a, nb_version):
+ kwargs = dict(name="FrontPort1", device=device_a.id, type="8p8c")
+ if version.parse(str(nb_version)) >= version.parse("4.5"):
+ kwargs["rear_ports"] = [{"rear_port": rear_port_a.id,
"rear_port_position": 1, "position": 1}]
+ else:
+ kwargs["rear_port"] = rear_port_a.id
+ kwargs["rear_port_position"] = 1
+ ret = api.dcim.front_ports.create(**kwargs)
yield ret
@pytest.fixture(scope="class")
@@ -348,14 +345,14 @@
yield ret
@pytest.fixture(scope="class")
- def front_port_b(self, api, device_b, rear_port_b):
- ret = api.dcim.front_ports.create(
- name="FrontPort2",
- device=device_b.id,
- type="8p8c",
- rear_port=rear_port_b.id,
- rear_port_position=1,
- )
+ def front_port_b(self, api, device_b, rear_port_b, nb_version):
+ kwargs = dict(name="FrontPort2", device=device_b.id, type="8p8c")
+ if version.parse(str(nb_version)) >= version.parse("4.5"):
+ kwargs["rear_ports"] = [{"rear_port": rear_port_b.id,
"rear_port_position": 1, "position": 1}]
+ else:
+ kwargs["rear_port"] = rear_port_b.id
+ kwargs["rear_port_position"] = 1
+ ret = api.dcim.front_ports.create(**kwargs)
yield ret
@pytest.fixture(scope="class")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/tests/test_api.py
new/pynetbox-7.7.0/tests/test_api.py
--- old/pynetbox-7.6.1/tests/test_api.py 2026-01-28 17:50:36.000000000
+0100
+++ new/pynetbox-7.7.0/tests/test_api.py 2026-05-05 23:36:52.000000000
+0200
@@ -94,11 +94,38 @@
class ApiCreateTokenTestCase(unittest.TestCase):
@patch(
"requests.sessions.Session.post",
- return_value=Response(fixture="api/token_provision.json"),
+ return_value=Response(fixture="api/token_provision_v1_legacy.json"),
)
- def test_create_token(self, *_):
+ def test_create_token_v1_legacy(self, *_):
+ """Old NetBox (pre-4.5): response has only 'key' field."""
api = pynetbox.api(host)
token = api.create_token("user", "pass")
self.assertTrue(isinstance(token, pynetbox.core.response.Record))
self.assertEqual(token.key, "1234567890123456789012345678901234567890")
self.assertEqual(api.token, "1234567890123456789012345678901234567890")
+
+ @patch(
+ "requests.sessions.Session.post",
+
return_value=Response(fixture="api/token_provision_v1_with_token.json"),
+ )
+ def test_create_token_v1_with_token(self, *_):
+ """NetBox 4.5+ v1 tokens: response includes 'token' field; use it
directly."""
+ api = pynetbox.api(host)
+ token = api.create_token("user", "pass")
+ self.assertTrue(isinstance(token, pynetbox.core.response.Record))
+ self.assertEqual(token.key, "1234567890123456789012345678901234567890")
+ self.assertEqual(api.token, "plaintexttoken7890abcdef1234567890abcdef")
+
+ @patch(
+ "requests.sessions.Session.post",
+ return_value=Response(fixture="api/token_provision_v2.json"),
+ )
+ def test_create_token_v2(self, *_):
+ """NetBox 4.5+ v2 tokens: auth token must be 'nbt_<key>.<token>'."""
+ api = pynetbox.api(host)
+ token = api.create_token("user", "pass")
+ self.assertTrue(isinstance(token, pynetbox.core.response.Record))
+ self.assertEqual(token.key, "shortkey1234567")
+ self.assertEqual(
+ api.token,
"nbt_shortkey1234567.plaintexttoken7890abcdef1234567890abcdef"
+ )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/tests/test_dcim.py
new/pynetbox-7.7.0/tests/test_dcim.py
--- old/pynetbox-7.6.1/tests/test_dcim.py 1970-01-01 01:00:00.000000000
+0100
+++ new/pynetbox-7.7.0/tests/test_dcim.py 2026-05-05 23:36:52.000000000
+0200
@@ -0,0 +1,88 @@
+import unittest
+from unittest.mock import patch
+
+import pynetbox
+from pynetbox.models.ipam import IpAddresses
+
+from .util import Response
+
+api = pynetbox.api(
+ "http://localhost:8000",
+)
+
+nb = api.dcim
+
+HEADERS = {"accept": "application/json"}
+
+
+class Generic:
+ class Tests(unittest.TestCase):
+ name = ""
+ ret = pynetbox.core.response.Record
+ app = "dcim"
+
+ def test_get_all(self):
+ with patch(
+ "requests.sessions.Session.get",
+ return_value=Response(fixture="{}/{}.json".format(self.app,
self.name)),
+ ) as mock:
+ ret = list(getattr(nb, self.name).all())
+ self.assertTrue(ret)
+ self.assertTrue(isinstance(ret[0], self.ret))
+ mock.assert_called_with(
+ "http://localhost:8000/api/{}/{}/".format(
+ self.app, self.name.replace("_", "-")
+ ),
+ params={"limit": 0},
+ json=None,
+ headers=HEADERS,
+ )
+
+ def test_filter(self):
+ with patch(
+ "requests.sessions.Session.get",
+ return_value=Response(fixture="{}/{}.json".format(self.app,
self.name)),
+ ) as mock:
+ ret = list(getattr(nb, self.name).filter(name="test"))
+ self.assertTrue(ret)
+ self.assertTrue(isinstance(ret[0], self.ret))
+ mock.assert_called_with(
+ "http://localhost:8000/api/{}/{}/".format(
+ self.app, self.name.replace("_", "-")
+ ),
+ params={"name": "test", "limit": 0},
+ json=None,
+ headers=HEADERS,
+ )
+
+ def test_get(self):
+ with patch(
+ "requests.sessions.Session.get",
+ return_value=Response(
+ fixture="{}/{}.json".format(self.app, self.name[:-1])
+ ),
+ ) as mock:
+ ret = getattr(nb, self.name).get(1)
+ self.assertTrue(ret)
+ self.assertTrue(isinstance(ret, self.ret))
+ mock.assert_called_with(
+ "http://localhost:8000/api/{}/{}/1/".format(
+ self.app, self.name.replace("_", "-")
+ ),
+ params={},
+ json=None,
+ headers=HEADERS,
+ )
+
+
+class DevicesTestCase(Generic.Tests):
+ name = "devices"
+
+ @patch(
+ "requests.sessions.Session.get",
+ return_value=Response(fixture="dcim/device.json"),
+ )
+ def test_oob_ip_attr(self, _):
+ device = nb.devices.get(1)
+ self.assertIsInstance(device.oob_ip, IpAddresses)
+ self.assertEqual(str(device.oob_ip), "192.0.2.1/32")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/tests/test_virtualization.py
new/pynetbox-7.7.0/tests/test_virtualization.py
--- old/pynetbox-7.6.1/tests/test_virtualization.py 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/tests/test_virtualization.py 2026-05-05
23:36:52.000000000 +0200
@@ -2,6 +2,8 @@
from unittest.mock import patch
import pynetbox
+from pynetbox.models.dcim import Devices
+from pynetbox.models.virtualization import VirtualMachineTypes
from .util import Response
@@ -89,6 +91,28 @@
class VirtualMachinesTestCase(Generic.Tests):
name = "virtual_machines"
+ @patch(
+ "requests.sessions.Session.get",
+ return_value=Response(fixture="virtualization/virtual_machine.json"),
+ )
+ def test_device_attr(self, _):
+ vm = nb.virtual_machines.get(1)
+ self.assertIsInstance(vm.device, Devices)
+ self.assertEqual(vm.device.name, "test-device")
+
+ @patch(
+ "requests.sessions.Session.get",
+ return_value=Response(fixture="virtualization/virtual_machine.json"),
+ )
+ def test_virtual_machine_type_attr(self, _):
+ vm = nb.virtual_machines.get(1)
+ self.assertIsInstance(vm.virtual_machine_type, VirtualMachineTypes)
+ self.assertEqual(vm.virtual_machine_type.name, "Standard")
+
+
+class VirtualMachineTypesTestCase(Generic.Tests):
+ name = "virtual_machine_types"
+
class InterfacesTestCase(Generic.Tests):
name = "interfaces"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/tests/unit/test_mapper.py
new/pynetbox-7.7.0/tests/unit/test_mapper.py
--- old/pynetbox-7.6.1/tests/unit/test_mapper.py 1970-01-01
01:00:00.000000000 +0100
+++ new/pynetbox-7.7.0/tests/unit/test_mapper.py 2026-05-05
23:36:52.000000000 +0200
@@ -0,0 +1,18 @@
+import unittest
+
+from pynetbox.models.mapper import CONTENT_TYPE_MAPPER
+from pynetbox.models.virtualization import VirtualMachineTypes
+
+
+class ContentTypeMapperTestCase(unittest.TestCase):
+ def test_cable_bundle_registered(self):
+ self.assertIn("dcim.cablebundle", CONTENT_TYPE_MAPPER)
+
+ def test_rack_group_registered(self):
+ self.assertIn("dcim.rackgroup", CONTENT_TYPE_MAPPER)
+
+ def test_virtual_machine_type_registered(self):
+ self.assertIn("virtualization.virtualmachinetype", CONTENT_TYPE_MAPPER)
+ self.assertIs(
+ CONTENT_TYPE_MAPPER["virtualization.virtualmachinetype"],
VirtualMachineTypes
+ )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pynetbox-7.6.1/tests/unit/test_response.py
new/pynetbox-7.7.0/tests/unit/test_response.py
--- old/pynetbox-7.6.1/tests/unit/test_response.py 2026-01-28
17:50:36.000000000 +0100
+++ new/pynetbox-7.7.0/tests/unit/test_response.py 2026-05-05
23:36:52.000000000 +0200
@@ -416,6 +416,100 @@
# This should now contain the bridge update
self.assertEqual(updates, {"bridge": 1})
+ def test_serialize_includes_new_field_set_to_none(self):
+ """Regression test for issue #708: serialize() should include fields
set after init."""
+ test_obj = Record({"id": 123, "name": "test"}, None, None)
+ test_obj.new_field = None
+
+ current = test_obj.serialize()
+ self.assertIn("new_field", current)
+ self.assertIsNone(current["new_field"])
+
+ init = test_obj.serialize(init=True)
+ self.assertNotIn("new_field", init)
+
+ def test_serialize_includes_new_field_set_to_value(self):
+ """Regression test for issue #708: serialize() should include all new
fields."""
+ test_obj = Record({"id": 123, "name": "test"}, None, None)
+ test_obj.new_string = "value"
+ test_obj.new_int = 42
+ test_obj.new_none = None
+
+ current = test_obj.serialize()
+ self.assertEqual(current["new_string"], "value")
+ self.assertEqual(current["new_int"], 42)
+ self.assertIsNone(current["new_none"])
+
+ init = test_obj.serialize(init=True)
+ self.assertNotIn("new_string", init)
+ self.assertNotIn("new_int", init)
+ self.assertNotIn("new_none", init)
+
+ def test_diff_detects_new_field_set_to_none(self):
+ """Regression test for issue #708: _diff() should detect new fields."""
+ test_obj = Record({"id": 123, "name": "test"}, None, None)
+ test_obj.primary_mac_address = None
+
+ diff = test_obj._diff()
+ self.assertIn("primary_mac_address", diff)
+
+ def test_updates_includes_new_field_set_to_none(self):
+ """Regression test for issue #708: updates() should include new fields
set to None."""
+ test_obj = Record({"id": 123, "name": "test"}, None, None)
+ test_obj.primary_mac_address = None
+
+ updates = test_obj.updates()
+ self.assertIn("primary_mac_address", updates)
+ self.assertIsNone(updates["primary_mac_address"])
+
+ def test_updates_includes_new_field_set_to_value(self):
+ """Regression test for issue #708: updates() should include all new
fields."""
+ test_obj = Record({"id": 123, "name": "test"}, None, None)
+ test_obj.new_field = "new_value"
+ test_obj.another_field = 42
+
+ updates = test_obj.updates()
+ self.assertEqual(updates["new_field"], "new_value")
+ self.assertEqual(updates["another_field"], 42)
+
+ def test_nested_object_field_update_issue_708(self):
+ """Regression test for issue #708: nested objects with limited
fields."""
+ api = Mock()
+ api.base_url = "http://localhost:8000/api"
+ interface = Record(
+ {
+ "id": 1,
+ "name": "eth0",
+ "url": "http://localhost:8000/api/dcim/interfaces/1/",
+ },
+ api,
+ None,
+ )
+ interface.primary_mac_address = None
+
+ updates = interface.updates()
+ self.assertIn("primary_mac_address", updates)
+ self.assertIsNone(updates["primary_mac_address"])
+
+ diff = interface._diff()
+ self.assertIn("primary_mac_address", diff)
+
+ def test_serialize_excludes_internal_attributes(self):
+ """Ensure serialize() filters out internal Record metadata."""
+ test_obj = Record({"id": 123, "name": "test"}, None, None)
+
+ serialized = test_obj.serialize()
+ for attr in [
+ "api",
+ "endpoint",
+ "url",
+ "has_details",
+ "default_ret",
+ "_init_cache",
+ "_full_cache",
+ ]:
+ self.assertNotIn(attr, serialized)
+
class RecordSetTestCase(unittest.TestCase):
ids = [1, 3, 5]