Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v2.1 #168

Merged
merged 19 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3953d78
Fixes upstream testing.
jvanderaa Dec 7, 2023
8259b6c
Merge pull request #160 from jvanderaa/fix-upstream-testing
jvanderaa Dec 7, 2023
83d8863
refactor: mismatching of signature of base method
nautics889 Jan 15, 2024
fde8446
Merge pull request #162 from nautics889/fix/miscellaneous-minor-fixes
joewesch Jan 17, 2024
e84e621
feature: added a bulk delete operation on the endpoint class
tsm1th Jan 29, 2024
4e8646e
Added unittest for endpoint delete method and extended error handling…
tsm1th Jan 30, 2024
051a270
Added unit tests which verify invalid type exceptions are raised corr…
tsm1th Jan 31, 2024
fb5d356
Refactored to pass CI
tsm1th Feb 5, 2024
7082244
Merge pull request #163 from vu-smitt13/feature/bulk-delete-endpoint-…
joewesch Feb 5, 2024
5e325bc
feature: bulk update implemented with unit tests
tsm1th Feb 1, 2024
9149e92
Added more unit tests for the endpoint.update() method
tsm1th Feb 5, 2024
e5cd0b0
Added backwards compatibility support to Endpoint.update() method
tsm1th Feb 5, 2024
ff54054
Fixed backwards comptibility support on the Endpoint.update() method …
tsm1th Feb 8, 2024
51c27df
Merge pull request #165 from vu-smitt13/feature/bulk-update-endpoint-…
joewesch Feb 8, 2024
33c20af
Update README.md
chadell Feb 14, 2024
7686036
Update README.md
chadell Feb 14, 2024
f17e47d
Merge pull request #166 from nautobot/chadell-patch-1
jvanderaa Feb 14, 2024
3b45c4a
Release v2.1.0
joewesch Feb 27, 2024
66ebf7f
Merge pull request #167 from nautobot/release-v2.1.0
jvanderaa Feb 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## v2.1.0

### New Features

- (#163) Adds `Endpoint.delete` method for bulk deleting of records
- (#165) Adds `Endpoint.update` method for bulk updating of records

### Fixes

- (#162) Corrects signature of `RODetailEndpoint.create` to provide a proper error that it is not implemented when using `api_version`

## v2.0.2

### Fixes
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ devices
Pynautobot provides a specialized `Endpoint` class to represent the Jobs model. This class is called `JobsEndpoint`.
This extends the `Endpoint` class by adding the `run` method so pynautobot can be used to call/execute a job run.

1. Run from a instance of a job.
1. Run from an instance of a job.

```python
>>> gc_backup_job = nautobot.extras.jobs.all()[14]
Expand All @@ -92,12 +92,12 @@ job.run(data={"hostname_regex": ".*"})
## Queries

Pynautobot provides several ways to retrieve objects from Nautobot.
Only the `get()` method is show here.
Only the `get()` method is shown here.
To continue from the example above, the `Endpoint` object returned will be used to `get`
the device named _hq-access-01_.

```python
switch = devices.get(nam="hq-access-01")
switch = devices.get(name="hq-access-01")
```

The object returned from the `get()` method is an implementation of the `Record` class.
Expand Down Expand Up @@ -126,7 +126,7 @@ nautobot = pynautobot.api(

### Versioning

Used for Nautobot Rest API versioning. Versioning can be controlled globally by setting `api_version` on initialization of the `API` class and/or for a specific request e.g (`list()`, `get()`, `create()` etc.) by setting an optional `api_version` parameter.
Used for Nautobot Rest API versioning. Versioning can be controlled globally by setting `api_version` on initialization of the `API` class and/or for a specific request e.g (`all()`, `filter()`, `get()`, `create()` etc.) by setting an optional `api_version` parameter.

**Global versioning**

Expand All @@ -135,7 +135,7 @@ import pynautobot
nautobot = pynautobot.api(
url="http://localhost:8000",
token="d6f4e314a5b5fefd164995169f28ae32d987704f",
api_version="1.3"
api_version="2.1"
)
```

Expand All @@ -147,13 +147,13 @@ nautobot = pynautobot.api(
url="http://localhost:8000", token="d6f4e314a5b5fefd164995169f28ae32d987704f",
)
tags = nautobot.extras.tags
tags.create(name="Tag", slug="tag", api_version="1.2",)
tags.list(api_version="1.3",)
tags.create(name="Tag", api_version="2.0", content_types=["dcim.device"])
tags.get(api_version="2.1",)
```

### Retry logic

By default, the client will not retry any operation. This behavior can be adjusted via the `retries` optional parameters. This will only affect for HTTP codes: 429, 500, 502, 503 and 504.
By default, the client will not retry any operation. This behavior can be adjusted via the `retries` optional parameters. This will only affect HTTP codes: 429, 500, 502, 503, and 504.

**Retries**

Expand Down
6 changes: 3 additions & 3 deletions development/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ services:
nautobot:
condition: "service_healthy"
nautobot:
image: "ghcr.io/nautobot/nautobot:${NAUTOBOT_VER:-1.3}-py${PYTHON_VER:-3.8}"
image: "ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER:-1.3}-py${PYTHON_VER:-3.8}"
command: "nautobot-server runserver 0.0.0.0:8000 --insecure"
ports:
- "8000:8000"
Expand All @@ -46,8 +46,8 @@ services:
image: "redis:6-alpine"
command:
- "sh"
- "-c" # this is to evaluate the $REDIS_PASSWORD from the env
- "redis-server --appendonly yes --requirepass $$REDIS_PASSWORD" ## $$ because of docker-compose
- "-c" # this is to evaluate the $REDIS_PASSWORD from the env
- "redis-server --appendonly yes --requirepass $$REDIS_PASSWORD" ## $$ because of docker-compose
env_file: "./dev.env"
docs:
image: "nginx:alpine"
Expand Down
28 changes: 28 additions & 0 deletions docs/advanced/delete.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Deleting Multiple Objects
=========================

The :ref:`Deleting Records` section shows how to use the
:py:meth:`~pynautobot.core.response.Record.delete` method to delete a single record.
Another way to accomplish this for multiple records at once is to use the
:py:meth:`~pynautobot.core.endpoint.Endpoint.delete` method.

.. code-block:: python

>>> import os
>>> from pynautobot import api
>>>
>>> url = os.environ["NAUTOBOT_URL"]
>>> token = os.environ["NAUTOBOT_TOKEN
>>> nautobot = api(url=url, token=token)
>>>
>>> # Delete multiple devices by passing a list of UUIDs
>>> device_uuids = [
>>> "a3e2f3e4-5b6c-4d5e-8f9a-1b2c3d4e5f6a",
>>> "b3e2f3e4-5b6c-4d5e-8f9a-1b2c3d4e5f6b",
>>> "c3e2f3e4-5b6c-4d5e-8f9a-1b2c3d4e5f6c",
>>> ]
>>> nautobot.dcim.devices.delete(device_uuids)
>>>
>>> # Delete all devices with a name starting with "Test"
>>> test_devices = nautobot.dcim.devices.filter(name__sw="Test")
>>> nautobot.dcim.devices.delete(test_devices)
31 changes: 31 additions & 0 deletions docs/advanced/update.rst
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,34 @@ The examples updates a Device record, however this can apply to other API
References:

* :ref:`Gathering Data from GraphQL Endpoint`

Updating Multiple Objects
-------------------------

The :py:meth:`~pynautobot.core.endpoint.Endpoint.update` method can also be used to update multiple
items with a single call. You can pass in a list of dictionaries, each containing the
``id`` and the fields to be updated, or a list of :py:class:`~pynautobot.core.response.Record`.

.. code-block:: python

>>> import os
>>> from pynautobot import api
>>>
>>> url = os.environ["NAUTOBOT_URL"]
>>> token = os.environ["NAUTOBOT_TOKEN
>>> nautobot = api(url=url, token=token)
>>>
>>> # Add a comment to multiple devices by passing in a list of dictionaries
>>> updated_devices = nautobot.dcim.devices.update([
>>> {"id": "db8770c4-61e5-4999-8372-e7fa576a4f65", "comments": "removed from service"},
>>> {"id": "e9b5f2e0-4f20-41ad-9179-90a4987f743e", "comments": "removed from service"},
>>> ])
>>>
>>> # Get a list of all devices
>>> devices = nautobot.dcim.devices.all()
>>> # Update the status and name fields for all records
>>> for device in devices:
>>> device.status = "Decommissioned"
>>> device.comments = "removed from service"
>>> # And then update them all at once
>>> updated_devices = nautobot.dcim.devices.update(devices)
190 changes: 165 additions & 25 deletions pynautobot/core/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

This file has been modified by NetworktoCode, LLC.
"""
from typing import Dict

from typing import List, Dict, Any
from uuid import UUID
from pynautobot.core.query import Request, RequestError
from pynautobot.core.response import Record

Expand Down Expand Up @@ -309,34 +311,71 @@ def create(self, *args, api_version=None, **kwargs):

return response_loader(req, self.return_obj, self)

def update(self, id: str, data: Dict[str, any]):
"""
Update a resource with a dictionary.
def update(self, *args, **kwargs):
r"""
Update a single resource with a dictionary or bulk update a list of objects.

Allows for bulk updating of existing objects on an endpoint.
Objects is a list which contain either json/dicts or Record
derived objects, which contain the updates to apply.
If json/dicts are used, then the id of the object *must* be
included

:arg list,optional \*args: A list of dicts or a list of Record

Accepts the id of the object that needs to be updated as well as
a dictionary of k/v pairs used to update an object. The object
is directly updated on the server using a PATCH request without
fetching object information.
:arg str,optional \**kwargs:
See Below

For fields requiring an object reference (such as a device location),
the API user is responsible for providing the object ID or the object
URL. This API will not accept the pynautobot object directly.
:Keyword Arguments:
* *id* (``string``) -- Identifier of the object being updated
* *data* (``dict``) -- k/v to update the record object with

:arg str id: Identifier of the object being updated
:arg dict data: Dictionary containing the k/v to update the
record object with.
:returns: True if PATCH request was successful.
:returns: A list or single :py:class:`.Record` object depending
on whether a bulk update was requested.
:example:

Accepts the id of the object that needs to be updated as well as a
dictionary of k/v pairs used to update an object
>>> nb.dcim.devices.update(id="0238a4e3-66f2-455a-831f-5f177215de0f", data={
... "name": "test-switch2",
... "serial": "ABC321",
... "name": "test",
... "serial": "1234",
... "location": "9b1f53c7-89fa-4fb2-a89a-b97364fef50c",
... })
True
>>>

Use bulk update by passing a list of dicts:

>>> devices = nb.dcim.devices.update([
... {'id': "db8770c4-61e5-4999-8372-e7fa576a4f65", 'name': 'test'},
... {'id': "e9b5f2e0-4f20-41ad-9179-90a4987f743e", 'name': 'test2'},
... ])
>>>

Use bulk update by passing a list of Records:

>>> devices = list(nb.dcim.devices.filter())
>>> devices
[Device1, Device2, Device3]
>>> for d in devices:
... d.name = d.name+'-test'
...
>>> nb.dcim.devices.update(devices)
>>>
"""
if not args and not kwargs:
raise ValueError("You must provide either a UUID and data dict or a list of objects to update")
uuid = kwargs.get("id", "")
data = kwargs.get("data", {})
if data and not uuid:
uuid = args[0]
if len(args) == 2:
uuid, data = args

if not any([uuid, data]):
return self.bulk_update(args[0])

req = Request(
key=id,
key=uuid,
base=self.url,
token=self.api.token,
http_session=self.api.http_session,
Expand All @@ -346,6 +385,103 @@ def update(self, id: str, data: Dict[str, any]):
return True
return False

def bulk_update(self, objects: List[Dict[str, Any]]):
r"""This method is called from the update() method if a bulk
update is detected.

Allows for bulk updating of existing objects on an endpoint.
Objects is a list which contain either json/dicts or Record
derived objects, which contain the updates to apply.
If json/dicts are used, then the id of the object *must* be
included

:arg list,optional \*args: A list of dicts or a list of Record
"""
if not isinstance(objects, list):
raise ValueError("objects must be a list[dict()|Record] not " + str(type(objects)))

bulk_data = []
for o in objects:
try:
if isinstance(o, dict):
bulk_data.append(o)
elif isinstance(o, Record):
if not hasattr(o, "id"):
raise ValueError("'Record' object has no attribute 'id'")
updates = o.updates()
if updates:
updates["id"] = o.id
bulk_data.append(updates)
else:
raise ValueError("Invalid object type: " + str(type(o)))
except ValueError as exc:
raise ValueError("Unexpected value in object list") from exc

req = Request(
base=self.url,
token=self.api.token,
http_session=self.api.http_session,
api_version=self.api.api_version,
).patch(bulk_data)
return response_loader(req, self.return_obj, self)

def delete(self, objects):
r"""Bulk deletes objects on an endpoint.

Allows for batch deletion of multiple objects from
a single endpoint

:arg list objects: A list of either ids or Records to delete.
:returns: True if bulk DELETE operation was successful.

:Examples:

Deleting all `devices`:

>>> pynautobot.dcim.devices.delete(pynautobot.dcim.devices.all())
>>>

Use bulk deletion by passing a list of ids:

>>> pynautobot.dcim.devices.delete(["db8770c4-61e5-4999-8372-e7fa576a4f65"
... ,"e9b5f2e0-4f20-41ad-9179-90a4987f743e"])
>>>

Use bulk deletion to delete objects eg. when filtering
on a `custom_field`:

>>> pynautobot.dcim.devices.delete([
... d for d in pynautobot.dcim.devices.all()
... if d.custom_fields.get("field", False)
... ])
>>>
"""
ids = []
if not isinstance(objects, list):
raise ValueError("objects must be a list[str(id)|Record] not " + str(type(objects)))
for o in objects:
try:
if isinstance(o, str):
if UUID(o):
ids.append(o)
elif isinstance(o, Record):
if not hasattr(o, "id"):
raise ValueError("'Record' object has no attribute 'id'")
ids.append(o.id)
else:
raise ValueError("Invalid object type: " + str(type(o)))
except ValueError as exc:
raise ValueError("Unexpected value in object list") from exc

req = Request(
base=self.url,
token=self.token,
http_session=self.api.http_session,
api_version=self.api.api_version,
)

return req.delete(data=[{"id": id} for id in ids])

def choices(self, api_version=None):
"""Returns all choices from the endpoint.

Expand Down Expand Up @@ -472,12 +608,16 @@ def list(self, api_version=None, **kwargs):
Returns the response from Nautobot for a detail endpoint.

Args:
:arg str,optional api_version: Override default or globally set Nautobot REST API version for this single request.
**kwargs: key/value pairs that get converted into url parameters when passed to the endpoint.
E.g. ``.list(method='get_facts')`` would be converted to ``.../?method=get_facts``.

:returns: A dictionary or list of dictionaries retrieved from
Nautobot.
:arg str,optional api_version: Override default or globally set Nautobot REST API version for this single request.
:arg \**kwargs:
See below

:Keyword Arugments:
key/value pairs that get converted into url parameters when passed to the endpoint.
E.g. ``.list(method='get_facts')`` would be converted to ``.../?method=get_facts``.

:returns: A dictionary or list of dictionaries retrieved from Nautobot.
"""
api_version = api_version or self.parent_obj.api.api_version

Expand Down Expand Up @@ -512,7 +652,7 @@ def create(self, data=None, api_version=None):


class RODetailEndpoint(DetailEndpoint):
def create(self, data):
def create(self, data=None, api_version=None):
raise NotImplementedError("Writes are not supported for this endpoint.")


Expand Down
Loading
Loading