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]

Reply via email to