From daa7a155279fad8d590226dc66b091805b027ca4 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 13 May 2025 15:51:37 -0700 Subject: [PATCH 1/6] Add branching support --- pynetbox/core/api.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pynetbox/core/api.py b/pynetbox/core/api.py index 6e6ccf52..a1307d72 100644 --- a/pynetbox/core/api.py +++ b/pynetbox/core/api.py @@ -14,6 +14,7 @@ limitations under the License. """ +import contextlib import requests from pynetbox.core.app import App, PluginsApp @@ -208,3 +209,21 @@ def create_token(self, username, password): # object details will fail self.token = resp["key"] return Record(resp, self, None) + + + @contextlib.contextmanager + def activate_branch(self, branch): + """ + Context manager to activate the branch by setting the schema ID in the headers. + + :raises ValueError: If the branch does not have a schema_id. + """ + if not isinstance(branch, Record) or not "schema_id" in dict(branch): + raise ValueError(f"The specified branch is not a valid NetBox branch: {branch}.") + + self.http_session.headers["X-NetBox-Branch"] = branch.schema_id + + try: + yield + finally: + self.http_session.headers.pop("X-NetBox-Branch", None) From cc3ed7b60aa6932387d209ede6eede31e6851ffd Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 14 May 2025 14:38:05 -0700 Subject: [PATCH 2/6] Add branching support --- pynetbox/core/api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pynetbox/core/api.py b/pynetbox/core/api.py index a1307d72..701a6593 100644 --- a/pynetbox/core/api.py +++ b/pynetbox/core/api.py @@ -15,6 +15,7 @@ """ import contextlib + import requests from pynetbox.core.app import App, PluginsApp @@ -210,7 +211,6 @@ def create_token(self, username, password): self.token = resp["key"] return Record(resp, self, None) - @contextlib.contextmanager def activate_branch(self, branch): """ @@ -219,7 +219,9 @@ def activate_branch(self, branch): :raises ValueError: If the branch does not have a schema_id. """ if not isinstance(branch, Record) or not "schema_id" in dict(branch): - raise ValueError(f"The specified branch is not a valid NetBox branch: {branch}.") + raise ValueError( + f"The specified branch is not a valid NetBox branch: {branch}." + ) self.http_session.headers["X-NetBox-Branch"] = branch.schema_id From 1defd485f616036a8ba831f2d4064bf8929ab584 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 16 May 2025 11:22:19 -0700 Subject: [PATCH 3/6] add documentation --- docs/branching.rst | 76 ++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + pynetbox/core/api.py | 11 ++++++- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 docs/branching.rst diff --git a/docs/branching.rst b/docs/branching.rst new file mode 100644 index 00000000..598fbb25 --- /dev/null +++ b/docs/branching.rst @@ -0,0 +1,76 @@ +Branching Plugin +================ + +The NetBox branching plugin allows you to create and work with branches in NetBox, similar to version control systems. This enables you to make changes in isolation and merge them back to the main branch when ready. + +Activating Branches +----------------- + +The `activate_branch` context manager allows you to perform operations within a specific branch's schema. All operations performed within the context manager will use that branch's schema. + +.. code-block:: python + + import pynetbox + + # Initialize the API + nb = pynetbox.api( + "http://localhost:8000", + token="your-token-here" + ) + + # Get a new branch + branch = nb.plugins.branching.branches.get(id=1) + + # Activate the branch for operations + with nb.activate_branch(branch): + # All operations within this block will use the branch's schema + sites = nb.dcim.sites.all() + # Make changes to objects... + # These changes will only exist in this branch + +Waiting for Branch Status +----------------------- + +When working with branches, you often need to wait for certain status changes, such as when a branch becomes ready after creation or when a merge operation completes. The `tenacity`_ library provides a robust way to handle these waiting scenarios. + +First, install tenacity: + +.. code-block:: bash + + pip install tenacity + +Here's how to use tenacity to wait for branch status changes: + +.. code-block:: python + + from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_result + + def is_branch_status(branch, target_status): + return str(branch.status) == target_status + + @retry( + stop=stop_after_attempt(30), + wait=wait_exponential(multiplier=1, min=4, max=60), + retry=retry_if_result(lambda x: not is_branch_status(x, target_status)) + ) + def wait_for_branch_status(branch, target_status): + """Wait for branch to reach a specific status, with exponential backoff.""" + branch = nb.plugins.branching.branches.get(branch.id) + return branch + + # Wait for a newly created branch to be ready + branch = nb.plugins.branching.branches.create(name="testbranch") + branch = wait_for_branch_status(branch, "Ready") + + # Or wait for a merge operation to complete + merge_result = nb.plugins.branching.branches.merge(branch) + branch = wait_for_branch_status(merge_result, "Ready") + +The retry configuration: +- Tries up to 30 times +- Uses exponential backoff starting at 4 seconds, up to 60 seconds +- Checks the branch status by fetching the latest branch data +- Continues retrying until the branch reaches the target status + +.. _tenacity: https://github.com/jd/tenacity + diff --git a/docs/index.rst b/docs/index.rst index 4deeac23..81563942 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,6 +6,7 @@ response request IPAM + branching advanced TL;DR diff --git a/pynetbox/core/api.py b/pynetbox/core/api.py index 701a6593..59598d07 100644 --- a/pynetbox/core/api.py +++ b/pynetbox/core/api.py @@ -216,7 +216,16 @@ def activate_branch(self, branch): """ Context manager to activate the branch by setting the schema ID in the headers. - :raises ValueError: If the branch does not have a schema_id. + :Raises: ValueError if the branch is not a valid NetBox branch. + + :Example: + + >>> import pynetbox + >>> nb = pynetbox.api("https://netbox-server") + >>> branch = nb.plugins.branching.branches.create(name="testbranch") + >>> with nb.activate_branch(branch): + ... sites = nb.dcim.sites.all() + ... # All operations within this block will use the branch's schema """ if not isinstance(branch, Record) or not "schema_id" in dict(branch): raise ValueError( From 58b89cafacf314ad6399fd5113aed807caccd82e Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 16 May 2025 11:23:12 -0700 Subject: [PATCH 4/6] add documentation --- docs/branching.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/branching.rst b/docs/branching.rst index 598fbb25..884105fa 100644 --- a/docs/branching.rst +++ b/docs/branching.rst @@ -64,7 +64,7 @@ Here's how to use tenacity to wait for branch status changes: # Or wait for a merge operation to complete merge_result = nb.plugins.branching.branches.merge(branch) - branch = wait_for_branch_status(merge_result, "Ready") + branch = wait_for_branch_status(merge_result, "Merged") The retry configuration: - Tries up to 30 times From 818ac8297479a020fc844d417236c76d01115635 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 16 May 2025 11:46:01 -0700 Subject: [PATCH 5/6] add documentation --- docs/branching.rst | 50 ++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/branching.rst b/docs/branching.rst index 884105fa..fb7ade9b 100644 --- a/docs/branching.rst +++ b/docs/branching.rst @@ -39,38 +39,44 @@ First, install tenacity: pip install tenacity -Here's how to use tenacity to wait for branch status changes: +Here's how to create a reusable function to wait for branch status changes: .. code-block:: python - from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_result - - def is_branch_status(branch, target_status): - return str(branch.status) == target_status - + from tenacity import retry, retry_if_result, stop_after_attempt, wait_exponential + import pynetbox + @retry( - stop=stop_after_attempt(30), - wait=wait_exponential(multiplier=1, min=4, max=60), - retry=retry_if_result(lambda x: not is_branch_status(x, target_status)) + stop=stop_after_attempt(30), # Try for up to 30 attempts + wait=wait_exponential( + multiplier=1, min=4, max=60 + ), # Wait between 4-60 seconds, increasing exponentially + retry=retry_if_result(lambda x: not x), # Retry if the status check returns False ) def wait_for_branch_status(branch, target_status): """Wait for branch to reach a specific status, with exponential backoff.""" branch = nb.plugins.branching.branches.get(branch.id) - return branch + return str(branch.status) == target_status + + # Example usage: + branch = nb.plugins.branching.branches.create(name="my-branch") - # Wait for a newly created branch to be ready - branch = nb.plugins.branching.branches.create(name="testbranch") - branch = wait_for_branch_status(branch, "Ready") + # Wait for branch to be ready + wait_for_branch_status(branch, "Ready") - # Or wait for a merge operation to complete - merge_result = nb.plugins.branching.branches.merge(branch) - branch = wait_for_branch_status(merge_result, "Merged") - -The retry configuration: -- Tries up to 30 times -- Uses exponential backoff starting at 4 seconds, up to 60 seconds -- Checks the branch status by fetching the latest branch data -- Continues retrying until the branch reaches the target status + # Get the latest branch status + branch = nb.plugins.branching.branches.get(branch.id) + print(f"Branch is now ready! Status: {branch.status}") + +The function will: +1. Check the current status of the branch +2. If the status doesn't match the target status, it will retry with exponential backoff +3. Continue retrying until either: + - The branch reaches the target status + - The maximum number of attempts (30) is reached + - The maximum wait time (60 seconds) is exceeded + +The exponential backoff ensures that we don't overwhelm the server with requests while still checking frequently enough to catch status changes quickly. .. _tenacity: https://github.com/jd/tenacity From f31afe81fc9a31ca0bc57ecdc965028c7686749e Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 19 May 2025 07:46:56 -0700 Subject: [PATCH 6/6] Update doc --- docs/branching.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/branching.rst b/docs/branching.rst index fb7ade9b..b1c989c3 100644 --- a/docs/branching.rst +++ b/docs/branching.rst @@ -18,7 +18,7 @@ The `activate_branch` context manager allows you to perform operations within a token="your-token-here" ) - # Get a new branch + # Get an existing branch branch = nb.plugins.branching.branches.get(id=1) # Activate the branch for operations