Skip to content

Commit

Permalink
Merge pull request #233 from tsm1th/u/tsm1th-add-graphql-run-method
Browse files Browse the repository at this point in the history
Added graphql run method for query objects
  • Loading branch information
joewesch authored Oct 8, 2024
2 parents c70b745 + 0fb0623 commit 1bd406c
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 3 deletions.
40 changes: 40 additions & 0 deletions docs/user/advanced/graphql.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,46 @@ location name is now derived using variables.
GraphQLRecord(json={'data': {'locations': [{'id': ..., 'name': 'HQ', 'parent': {'name': 'US'}}]}}, status_code=200)
```

## Making a GraphQL Query with a Saved Query

Nautobot supports saving your graphql queries and executing them later.
Here is an example of saving a query using pynautobot.

```python
>>> query = """
... query ($location_name:String!) {
... locations (name: [$location_name]) {
... id
... name
... parent {
... name
... }
... }
... }
... """
>>> data = {"name": "Foobar", "query": query}
>>> nautobot.extras.graphql_queries.create(**data)
```

Now that we have a query saved with the name `Foobar`, we can execute it using pynautobot.

```python
>>> # Create a variables dictionary
>>> variables = {"location_name": "HQ"}
>>>
>>> # Get the query object that was created
>>> query_object = nautobot.extras.graphql_queries.get("Foobar")
>>>
>>> # Call the run method to execute query
>>> query_object.run()
>>>
>>> # To execute a query with variables
>>> query_object.run(variables=variables)
>>>
>>> # To execute a query with custom payload
>>> query_object.run({"variables": variables, "foo": "bar"})
```

## The GraphQLRecord Object

The `~pynautobot.core.graphql.GraphQLRecord`{.interpreted-text
Expand Down
4 changes: 3 additions & 1 deletion pynautobot/core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import logging

from pynautobot.core.endpoint import Endpoint, JobsEndpoint
from pynautobot.core.endpoint import Endpoint, JobsEndpoint, GraphqlEndpoint
from pynautobot.core.query import Request
from pynautobot.models import circuits, dcim, extras, ipam, users, virtualization

Expand Down Expand Up @@ -63,6 +63,8 @@ def __setstate__(self, d):
def __getattr__(self, name):
if name == "jobs":
return JobsEndpoint(self.api, self, name, model=self.model)
elif name == "graphql_queries":
return GraphqlEndpoint(self.api, self, name, model=self.model)
return Endpoint(self.api, self, name, model=self.model)

def __dir__(self):
Expand Down
44 changes: 44 additions & 0 deletions pynautobot/core/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,3 +690,47 @@ def run(self, *args, api_version=None, **kwargs):
).post(args[0] if args else kwargs)

return response_loader(req, self.return_obj, self)


class GraphqlEndpoint(Endpoint):
"""Extend Endpoint class to support run method for graphql queries."""

def run(self, query_id, *args, **kwargs):
"""Runs a saved graphql query based on the query_id provided.
Takes a kwarg of `query_id` to specify the query that should be run.
Args:
*args (str, optional): Used as payload for POST method
to the API if provided.
**kwargs (str, optional): Any additional argument the
endpoint accepts can be added as a keyword arg.
query_id (str, required): The UUID of the query object
that is being ran.
Returns:
An API response from the execution of the saved graphql query.
Examples:
To run a query no variables:
>>> query = nb.extras.graphql_queries.get("Example")
>>> query.run()
To run a query with `variables` as kwarg:
>>> query = nb.extras.graphql_queries.get("Example")
>>> query.run(
variables={"foo": "bar"})
)
To run a query with JSON payload as an arg:
>>> query = nb.extras.graphql_queries.get("Example")
>>> query.run(
{"variables":{"foo":"bar"}}
)
"""
query_run_url = f"{self.url}/{query_id}/run/"
return Request(
base=query_run_url,
token=self.token,
http_session=self.api.http_session,
).post(args[0] if args else kwargs)
8 changes: 7 additions & 1 deletion pynautobot/models/extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#
# This file has been modified by NetworktoCode, LLC.

from pynautobot.core.endpoint import JobsEndpoint, DetailEndpoint
from pynautobot.core.endpoint import JobsEndpoint, DetailEndpoint, GraphqlEndpoint
from pynautobot.core.response import JsonField, Record


Expand Down Expand Up @@ -44,6 +44,12 @@ def run(self, **kwargs):
return JobsEndpoint(self.api, self.api.extras, "jobs").run(class_path=self.id, **kwargs)


class GraphqlQueries(Record):
def run(self, *args, **kwargs):
"""Run a graphql query from a saved graphql instance."""
return GraphqlEndpoint(self.api, self.api.extras, "graphql_queries").run(self.id, *args, **kwargs)


class DynamicGroups(Record):
def __str__(self):
parent_record_string = super().__str__()
Expand Down
29 changes: 29 additions & 0 deletions tests/integration/test_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,32 @@ def test_get_note_on_invalid_object(self, nb_client):
test_obj = nb_client.extras.content_types.get(model="manufacturer")
with pytest.raises(Exception, match="The requested url: .* could not be found."):
test_obj.notes.list()


class TestGraphqlQueries:
"""Verify we can create and run a saved graphql query"""

@pytest.fixture(scope="session")
def create_graphql_query(self, nb_client):
query = """
query Example($devicename: [String] = "server.networktocode.com"){
devices(name: $devicename) {
name
serial
}
}
"""
data = {"name": "foobar", "query": query}
return nb_client.extras.graphql_queries.create(**data)

def test_graphql_query_run(self, create_graphql_query):
query = create_graphql_query
data = query.run()
assert len(data.get("data", {}).get("devices")) == 1
assert data.get("data", {}).get("devices")[0].get("name") == "server.networktocode.com"

def test_graphql_query_run_with_variable(self, create_graphql_query):
query = create_graphql_query
data = query.run(variables={"devicename": "dev-1"})
assert len(data.get("data", {}).get("devices")) == 1
assert data.get("data", {}).get("devices")[0].get("name") == "dev-1"
13 changes: 12 additions & 1 deletion tests/unit/test_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest
from unittest.mock import Mock, patch

from pynautobot.core.endpoint import Endpoint, JobsEndpoint
from pynautobot.core.endpoint import Endpoint, JobsEndpoint, GraphqlEndpoint
from pynautobot.core.response import Record


Expand Down Expand Up @@ -248,3 +248,14 @@ def test_run_greater_v1_3(self):
test_obj = JobsEndpoint(api, app, "test")
test = test_obj.run(job_id="test")
self.assertEqual(len(test), 1)


class GraphqlEndPointTestCase(unittest.TestCase):
def test_invalid_arg(self):
with self.assertRaises(
TypeError, msg="GraphqlEndpoint.run() missing 1 required positional argument: 'query_id'"
):
api = Mock(base_url="http://localhost:8000/api")
app = Mock(name="test")
test_obj = GraphqlEndpoint(api, app, "test")
test_obj.run()

0 comments on commit 1bd406c

Please sign in to comment.